feat: add certificate preference to name fields

Adds a checkbox under "Name" and "Verified Name" fields,
signifying which name the user prefers to display on their
certificates. When checking the checkbox, the user can save
this choice along with their name. When unchecking the check-
box, a modal appears, prompting the user to choose a name
to display on their certificate.
This commit is contained in:
Bianca Severino
2021-09-08 13:21:29 -04:00
parent 53b59231cb
commit cc65ffc96f
10 changed files with 641 additions and 64 deletions

View File

@@ -19,7 +19,12 @@ import {
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
import messages from './AccountSettingsPage.messages';
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
import {
fetchSettings,
saveMultipleSettings,
saveSettings,
updateDraft,
} from './data/actions';
import { accountSettingsPageSelector } from './data/selectors';
import PageLoading from './PageLoading';
import JumpNav from './JumpNav';
@@ -162,6 +167,23 @@ class AccountSettingsPage extends React.Component {
this.props.saveSettings(formId, values);
}
handleSubmitName = (formId, values) => {
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
this.props.saveMultipleSettings([
{
formId,
commitValues: values,
},
{
formId: 'useVerifiedNameForCerts',
commitValues: this.props.formValues.useVerifiedNameForCerts,
},
], formId);
} else {
this.props.saveSettings(formId, values);
}
};
isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName);
}
@@ -224,6 +246,25 @@ class AccountSettingsPage extends React.Component {
);
}
renderFullNameHelpText = (status) => {
if (
!this.props.formValues.verifiedNameHistory
|| !this.props.formValues.verifiedNameHistory.verified_name_enabled
) {
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']);
}
switch (status) {
case 'submitted':
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted']);
default:
if (this.props.committedValues.useVerifiedNameForCerts) {
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.non.certificate']);
}
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.certificate']);
}
}
renderVerifiedNameSuccessMessage = () => (
<OneTimeDismissibleAlert
id="dismissedVerifiedNameSuccessMessage"
@@ -312,6 +353,9 @@ class AccountSettingsPage extends React.Component {
renderVerifiedNameHelpText = (status) => {
switch (status) {
case 'approved':
if (this.props.committedValues.useVerifiedNameForCerts) {
return (this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.certificate']));
}
return (this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.verified']));
case 'submitted':
return (this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.submitted']));
@@ -320,15 +364,6 @@ class AccountSettingsPage extends React.Component {
}
}
renderFullNameHelpText = (status) => {
switch (status) {
case 'submitted':
return (this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted']));
default:
return (this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']));
}
}
renderEmptyStaticFieldMessage() {
if (this.isManagedProfile()) {
return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], {
@@ -440,7 +475,8 @@ class AccountSettingsPage extends React.Component {
isGrayedOut={
verifiedNameEnabled && verifiedName && !this.isEditable('verifiedName')
}
{...editableFieldProps}
onChange={this.handleEditableFieldChange}
onSubmit={this.handleSubmitName}
/>
{verifiedNameEnabled && verifiedName
&& (
@@ -461,7 +497,8 @@ class AccountSettingsPage extends React.Component {
helpText={this.renderVerifiedNameHelpText(verifiedName.status)}
isEditable={this.isEditable('verifiedName')}
isGrayedOut={!this.isEditable('verifiedName')}
{...(this.isEditable('verifiedName') && editableFieldProps)}
onChange={this.handleEditableFieldChange}
onSubmit={this.handleSubmitName}
/>
)}
@@ -739,6 +776,7 @@ AccountSettingsPage.propTypes = {
}),
state: PropTypes.string,
shouldDisplayDemographicsSection: PropTypes.bool,
useVerifiedNameForCerts: PropTypes.bool.isRequired,
verifiedNameHistory: PropTypes.shape({
verified_name_enabled: PropTypes.bool,
use_verified_name_for_certs: PropTypes.bool,
@@ -758,6 +796,10 @@ AccountSettingsPage.propTypes = {
status: PropTypes.string,
}),
}).isRequired,
committedValues: PropTypes.shape({
useVerifiedNameForCerts: PropTypes.bool,
}),
drafts: PropTypes.shape({}),
siteLanguage: PropTypes.shape({
previousValue: PropTypes.string,
draft: PropTypes.string,
@@ -781,6 +823,7 @@ AccountSettingsPage.propTypes = {
})),
fetchSiteLanguages: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
saveMultipleSettings: PropTypes.func.isRequired,
saveSettings: PropTypes.func.isRequired,
fetchSettings: PropTypes.func.isRequired,
tpaProviders: PropTypes.arrayOf(PropTypes.object),
@@ -790,6 +833,10 @@ AccountSettingsPage.defaultProps = {
loading: false,
loaded: false,
loadingError: null,
committedValues: {
useVerifiedNameForCerts: false,
},
drafts: {},
siteLanguage: null,
siteLanguageOptions: [],
timeZoneOptions: [],
@@ -804,6 +851,7 @@ AccountSettingsPage.defaultProps = {
export default connect(accountSettingsPageSelector, {
fetchSettings,
saveSettings,
saveMultipleSettings,
updateDraft,
fetchSiteLanguages,
})(injectIntl(AccountSettingsPage));

View File

@@ -91,6 +91,16 @@ const messages = defineMessages({
defaultMessage: 'The name that is used for ID verification and that appears on your certificates.',
description: 'Help text for the account settings name field.',
},
'account.settings.field.full.name.help.text.non.certificate': {
id: 'account.settings.field.full.name.help.text.non.certificate',
defaultMessage: 'The name that appears on your public profile.',
description: 'Help text for the account settings name field.',
},
'account.settings.field.full.name.help.text.certificate': {
id: 'account.settings.field.full.name.help.text.certificate',
defaultMessage: 'This name is selected to appear on your certificates and public-facing records.',
description: 'Help text for the account settings name field.',
},
'account.settings.field.name.verified': {
id: 'account.settings.field.name.verified',
defaultMessage: 'Verified name',
@@ -101,6 +111,11 @@ const messages = defineMessages({
defaultMessage: 'This name has been verified by government ID.',
description: 'Help text for the account settings verified name field when the name is verified.',
},
'account.settings.field.name.verified.help.text.certificate': {
id: 'account.settings.field.name.verified.help.text.certificate',
defaultMessage: 'This name has been verified by government ID and selected to appear on your certificates and public-facing records.',
description: 'Help text for the account settings verified name field when the name is selected for certificates.',
},
'account.settings.field.name.verified.help.text.submitted': {
id: 'account.settings.field.name.verified.help.text.submitted',
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.',

View File

@@ -16,6 +16,7 @@ import {
closeForm,
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
import CertificatePreference from './certificate-preference/CertificatePreference';
function EditableField(props) {
const {
@@ -103,54 +104,57 @@ function EditableField(props) {
expression={isEditing ? 'editing' : 'default'}
cases={{
editing: (
<form onSubmit={handleSubmit}>
<ValidationFormGroup
for={id}
invalid={error != null}
invalidMessage={error}
helpText={helpText}
>
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
data-hj-suppress
name={name}
id={id}
type={type}
value={value}
onChange={handleChange}
options={options}
{...others}
/>
<>{others.children}</>
</ValidationFormGroup>
<p>
<StatefulButton
type="submit"
className="mr-2"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
onClick={(e) => {
// Swallow clicks if the state is pending.
// We do this instead of disabling the button to prevent
// it from losing focus (disabled elements cannot have focus).
// Disabling it would causes upstream issues in focus management.
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
if (saveState === 'pending') { e.preventDefault(); }
}}
disabledStates={[]}
/>
<Button
variant="outline-primary"
onClick={handleCancel}
<>
<form onSubmit={handleSubmit}>
<ValidationFormGroup
for={id}
invalid={error != null}
invalidMessage={error}
helpText={helpText}
>
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button>
</p>
</form>
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
data-hj-suppress
name={name}
id={id}
type={type}
value={value}
onChange={handleChange}
options={options}
{...others}
/>
<>{others.children}</>
</ValidationFormGroup>
<p>
<StatefulButton
type="submit"
className="mr-2"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
onClick={(e) => {
// Swallow clicks if the state is pending.
// We do this instead of disabling the button to prevent
// it from losing focus (disabled elements cannot have focus).
// Disabling it would causes upstream issues in focus management.
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
if (saveState === 'pending') { e.preventDefault(); }
}}
disabledStates={[]}
/>
<Button
variant="outline-primary"
onClick={handleCancel}
>
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button>
</p>
</form>
{['name', 'verifiedName'].includes(name) && <CertificatePreference fieldName={name} />}
</>
),
default: (
<div className="form-group">

View File

@@ -0,0 +1,175 @@
import React, { useState, useEffect } from 'react';
import { connect, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
ActionRow,
Form,
ModalDialog,
StatefulButton,
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
closeForm,
resetDrafts,
saveSettings,
updateDraft,
} from '../data/actions';
import { certPreferenceSelector } from '../data/selectors';
import commonMessages from '../AccountSettingsPage.messages';
import messages from './messages';
function CertificatePreference({
intl,
fieldName,
originalFullName,
originalVerifiedName,
saveState,
useVerifiedNameForCerts,
verifiedNameEnabled,
}) {
if (!verifiedNameEnabled || !originalVerifiedName) {
// If the user doesn't have an approved verified name, do not display this component
return null;
}
const dispatch = useDispatch();
const [checked, setChecked] = useState(false);
const [modalIsOpen, setModalIsOpen] = useState(false);
const formId = 'useVerifiedNameForCerts';
function handleCheckboxChange() {
if (!checked) {
if (fieldName === 'verifiedName') {
dispatch(updateDraft(formId, true));
} else {
dispatch(updateDraft(formId, false));
}
} else {
setModalIsOpen(true);
}
}
function handleCancel() {
setModalIsOpen(false);
dispatch(resetDrafts());
}
function handleModalChange(e) {
if (e.target.value === 'fullName') {
dispatch(updateDraft(formId, false));
} else {
dispatch(updateDraft(formId, true));
}
}
function handleSubmit(e) {
e.preventDefault();
if (saveState === 'pending') {
return;
}
dispatch(saveSettings(formId, useVerifiedNameForCerts));
}
useEffect(() => {
if (fieldName === 'verifiedName') {
setChecked(useVerifiedNameForCerts);
} else {
setChecked(!useVerifiedNameForCerts);
}
}, [useVerifiedNameForCerts]);
useEffect(() => {
if (modalIsOpen && saveState === 'complete') {
setModalIsOpen(false);
dispatch(closeForm(fieldName));
}
}, [modalIsOpen, saveState]);
return (
<>
<Form.Checkbox className="mt-1 mb-4" checked={checked} onChange={handleCheckboxChange}>
{intl.formatMessage(messages['account.settings.field.name.checkbox.certificate.select'])}
</Form.Checkbox>
<ModalDialog
title={intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
isOpen={modalIsOpen}
onClose={handleCancel}
size="lg"
hasCloseButton
isFullscreenOnMobile
>
<Form onSubmit={handleSubmit}>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<Form.Group className="mb-4">
<Form.Label>
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.select'])}
</Form.Label>
<Form.RadioSet
name={formId}
value={useVerifiedNameForCerts ? 'verifiedName' : 'fullName'}
onChange={handleModalChange}
>
<Form.Radio value="fullName">
{originalFullName}{' '}
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.full'])})
</Form.Radio>
<Form.Radio value="verifiedName">
{originalVerifiedName}{' '}
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.verified'])})
</Form.Radio>
</Form.RadioSet>
</Form.Group>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="outline-primary" disabled={saveState === 'pending'}>
{intl.formatMessage(commonMessages['account.settings.editable.field.action.cancel'])}
</ModalDialog.CloseButton>
<StatefulButton
type="submit"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.field.name.modal.certificate.button.choose']),
}}
disabledStates={[]}
/>
</ActionRow>
</ModalDialog.Footer>
</Form>
</ModalDialog>
</>
);
}
CertificatePreference.propTypes = {
intl: intlShape.isRequired,
fieldName: PropTypes.string.isRequired,
originalFullName: PropTypes.string,
originalVerifiedName: PropTypes.string,
saveState: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
verifiedNameEnabled: PropTypes.bool,
};
CertificatePreference.defaultProps = {
originalFullName: '',
originalVerifiedName: '',
saveState: null,
useVerifiedNameForCerts: false,
verifiedNameEnabled: false,
};
export default connect(certPreferenceSelector)(injectIntl(CertificatePreference));

View File

@@ -0,0 +1,22 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { handleRequestError } from '../../data/utils';
// eslint-disable-next-line import/prefer-default-export
export async function postVerifiedNameConfig(username, commitValues) {
const requestConfig = { headers: { Accept: 'application/json' } };
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/config`;
const { useVerifiedNameForCerts } = commitValues;
const postValues = {
username,
use_verified_name_for_certs: useVerifiedNameForCerts,
};
const { data } = await getAuthenticatedHttpClient()
.post(requestUrl, postValues, requestConfig)
.catch(error => handleRequestError(error));
return data;
}

View File

@@ -0,0 +1,36 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.field.name.checkbox.certificate.select': {
id: 'account.settings.field.name.certificate.select',
defaultMessage: 'If checked, this name will appear on your certificates and public-facing records.',
description: 'Label for checkbox describing that the selected name will appear on the users certificates.',
},
'account.settings.field.name.modal.certificate.title': {
id: 'account.settings.field.name.modal.certificate.title',
defaultMessage: 'Choose a preferred name for certificates and public-facing records',
description: 'Title instructing the user to choose a preferred name.',
},
'account.settings.field.name.modal.certificate.select': {
id: 'account.settings.field.name.modal.certificate.select',
defaultMessage: 'Select a name',
description: 'Label instructing the user to select a name.',
},
'account.settings.field.name.modal.certificate.option.full': {
id: 'account.settings.field.name.modal.certificate.option.full',
defaultMessage: 'Full Name',
description: 'Option representing the users full name.',
},
'account.settings.field.name.modal.certificate.option.verified': {
id: 'account.settings.field.name.modal.certificate.option.verified',
defaultMessage: 'Verified Name',
description: 'Option representing the users verified name.',
},
'account.settings.field.name.modal.certificate.button.choose': {
id: 'account.settings.field.name.modal.certificate.button.choose',
defaultMessage: 'Choose name',
description: 'Button to confirm the users name choice.',
},
});
export default messages;

View File

@@ -0,0 +1,161 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
import CertificatePreference from '../CertificatePreference'; // eslint-disable-line import/first
const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
const history = createMemoryHistory();
const IntlCertificatePreference = injectIntl(CertificatePreference);
const mockStore = configureStore();
describe('NameChange', () => {
let props = {};
let store = {};
const formId = 'useVerifiedNameForCerts';
const updateDraft = 'UPDATE_DRAFT';
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
const reduxWrapper = children => (
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
</Router>
);
beforeEach(() => {
store = mockStore();
props = {
fieldName: 'name',
originalFullName: 'Ed X',
originalVerifiedName: 'edX Verified',
saveState: null,
useVerifiedNameForCerts: false,
verifiedNameEnabled: true,
intl: {},
};
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
});
afterEach(() => jest.clearAllMocks());
it('does not render if there is no verified name', () => {
props = {
...props,
originalVerifiedName: '',
};
const wrapper = render(reduxWrapper(<IntlCertificatePreference {...props} />));
expect(wrapper).toMatchSnapshot();
});
it('does not trigger modal when checking empty checkbox, and updates draft immediately', () => {
props = {
...props,
useVerifiedNameForCerts: true,
};
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
expect(checkbox.checked).toEqual(false);
fireEvent.click(checkbox);
expect(screen.queryByRole('radiogroup')).toBeNull();
expect(mockDispatch).toHaveBeenCalledWith({
payload: { name: formId, value: false },
type: updateDraft,
});
});
it('triggers modal when attempting to uncheck checkbox', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
expect(checkbox.checked).toEqual(true);
fireEvent.click(checkbox);
expect(mockDispatch).not.toHaveBeenCalled();
screen.getByRole('radiogroup');
});
it('updates draft when changing radio value', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const fullNameOption = screen.getByLabelText('Ed X (Full Name)');
const verifiedNameOption = screen.getByLabelText('edX Verified (Verified Name)');
expect(fullNameOption.checked).toEqual(true);
expect(verifiedNameOption.checked).toEqual(false);
fireEvent.click(verifiedNameOption);
expect(mockDispatch).toHaveBeenCalledWith({
payload: { name: formId, value: true },
type: updateDraft,
});
});
it('clears draft on cancel', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const cancelButton = screen.getByText('Cancel');
fireEvent.click(cancelButton);
expect(mockDispatch).toHaveBeenCalledWith({ type: 'RESET_DRAFTS' });
expect(screen.queryByRole('radiogroup')).toBeNull();
});
it('submits', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const submitButton = screen.getByText('Choose name');
fireEvent.click(submitButton);
expect(mockDispatch).toHaveBeenCalledWith({
payload: { formId, commitValues: false },
type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS',
});
});
});

View File

@@ -0,0 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NameChange does not render if there is no verified name 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div />
</body>,
"container": <div />,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;

View File

@@ -58,12 +58,22 @@ const valuesSelector = createSelector(
accountSettingsSelector,
mostRecentVerifiedNameSelector,
mostRecentApprovedVerifiedNameValueSelector,
(accountSettings, mostRecentVerifiedNameValue, mostRecentApprovedVerifiedNameValue) => (
{
(accountSettings, mostRecentVerifiedNameValue, mostRecentApprovedVerifiedNameValue) => {
let useVerifiedNameForCerts = (
accountSettings.values.verifiedNameHistory?.use_verified_name_for_certs || false
);
if (Object.keys(accountSettings.confirmationValues).includes('useVerifiedNameForCerts')) {
useVerifiedNameForCerts = accountSettings.confirmationValues.useVerifiedNameForCerts;
}
return {
...accountSettings.values,
verifiedName: mostRecentApprovedVerifiedNameValue,
mostRecentVerifiedName: mostRecentVerifiedNameValue,
}),
useVerifiedNameForCerts,
};
},
);
const draftsSelector = createSelector(
@@ -150,7 +160,11 @@ const formValuesSelector = createSelector(
(values, drafts) => {
const formValues = {};
Object.entries(values).forEach(([name, value]) => {
formValues[name] = chooseFormValue(drafts[name], value) || '';
if (typeof value === 'boolean') {
formValues[name] = chooseFormValue(drafts[name], value);
} else {
formValues[name] = chooseFormValue(drafts[name], value) || '';
}
});
return formValues;
},
@@ -195,6 +209,8 @@ export const accountSettingsPageSelector = createSelector(
siteLanguageOptionsSelector,
siteLanguageSelector,
formValuesSelector,
valuesSelector,
draftsSelector,
profileDataManagerSelector,
staticFieldsSelector,
timeZonesSelector,
@@ -205,6 +221,8 @@ export const accountSettingsPageSelector = createSelector(
siteLanguageOptions,
siteLanguage,
formValues,
committedValues,
drafts,
profileDataManager,
staticFields,
timeZoneOptions,
@@ -220,12 +238,36 @@ export const accountSettingsPageSelector = createSelector(
countryTimeZoneOptions,
isActive: activeAccount,
formValues,
committedValues,
drafts,
profileDataManager,
staticFields,
tpaProviders: accountSettings.thirdPartyAuth.providers,
}),
);
export const certPreferenceSelector = createSelector(
valuesSelector,
formValuesSelector,
mostRecentApprovedVerifiedNameValueSelector,
saveStateSelector,
errorSelector,
(
committedValues,
formValues,
mostRecentApprovedVerifiedNameValue,
saveState,
errors,
) => ({
originalFullName: committedValues?.name || '',
originalVerifiedName: mostRecentApprovedVerifiedNameValue?.verified_name || '',
useVerifiedNameForCerts: formValues.useVerifiedNameForCerts || false,
saveState,
verifiedNameEnabled: formValues.verifiedNameHistory?.verified_name_enabled || false,
formErrors: errors,
}),
);
export const coachingConsentPageSelector = createSelector(
accountSettingsSelector,
formValuesSelector,

View File

@@ -7,6 +7,7 @@ import isEmpty from 'lodash.isempty';
import { handleRequestError, unpackFieldErrors } from './utils';
import { getThirdPartyAuthProviders } from '../third-party-auth';
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
@@ -256,11 +257,19 @@ export async function patchSettings(username, commitValues, userId) {
const preferenceKeys = ['time_zone'];
const coachingKeys = ['coaching'];
const demographicsKeys = DEMOGRAPHICS_FIELDS;
const certificateKeys = ['useVerifiedNameForCerts'];
const isDemographicsKey = (value, key) => key.includes('demographics');
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys, demographicsKeys);
const accountCommitValues = omit(
commitValues,
preferenceKeys,
coachingKeys,
demographicsKeys,
certificateKeys,
);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys);
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
const certCommitValues = pick(commitValues, certificateKeys);
const patchRequests = [];
if (!isEmpty(accountCommitValues)) {
@@ -275,6 +284,9 @@ export async function patchSettings(username, commitValues, userId) {
if (!isEmpty(demographicsCommitValues)) {
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
}
if (!isEmpty(certCommitValues)) {
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
}
const results = await Promise.all(patchRequests);
// Assigns in order of requests. Preference keys