feat: Remove Use of Verified Name Enabled Flag
The VERIFIED_NAME_FLAG was added as part https://github.com/edx/edx-name-affirmation/pull/12, [MST-801](https://openedx.atlassian.net/browse/MST-801) in order to control the release of the Verified Name project. It was used for a phased roll out by percentage of users. The release reached a percentage of 50% before it was observed that, due to the way percentage roll out works in django-waffle, the code to create or update VerifiedName records was not working properly. The code was written such that any change to a SoftwareSecurePhotoVerification model instance sent a signal, which was received and handled by the Name Affirmation application. If the VERIFIED_NAME_FLAG was on for the requesting user, a Celery task was launched from the Name Affirmation application to perform the creation of or update to the appropriate VerifiedName model instances based on the verify_student application signal. However, we observed that when SoftwareSecurePhotoVerification records were moved into the "created" or "ready" status, a Celery task in Name Affirmation was created, but when SoftwareSecurePhotoVerification records were moved into the "submitted" status, the corresponding Celery task in Name Affirmation was not created. This caused VerifiedName records to stay in the "pending" state. The django-waffle waffle flag used by the edx-toggle library implements percentage rollout by setting a cookie in a learner's browser session to assign them to the enabled or disabled group. It turns out that the code that submits a SoftwareSecurePhotoVerification record, which moves it into the "submitted" state, happens as part of a Celery task in the verify_student application in the edx-platform. Therefore, we believe that because there is no request object in a Celery task, the edx-toggle code is defaulting to the case where there is no request object. In this case, the code checks whether the flag is enabled for everyone when determining whether the flag is enabled. Because of the percentage rollout (i.e. waffle flag not enabled for everyone), the Celery task in Name Affirmation is not created. This behavior was confirmed by logging added as part of https://github.com/edx/edx-name-affirmation/pull/62. We have determined that we do not need the waffle flag, as we are comfortable that enabling the waffle flag for everyone will fix the issue and are comfortable releasing the feature to all users. For this reason, we are removing references to the flag. [MST-1130](https://openedx.atlassian.net/browse/MST-1130)
This commit is contained in:
@@ -262,7 +262,6 @@ class AccountSettingsPage extends React.Component {
|
||||
renderFullNameHelpText = (status) => {
|
||||
if (
|
||||
!this.props.verifiedNameHistory
|
||||
|| !this.props.verifiedNameEnabled
|
||||
) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']);
|
||||
}
|
||||
@@ -471,7 +470,7 @@ class AccountSettingsPage extends React.Component {
|
||||
// Show State field only if the country is US (could include Canada later)
|
||||
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
|
||||
|
||||
const { verifiedName, verifiedNameEnabled } = this.props;
|
||||
const { verifiedName } = this.props;
|
||||
|
||||
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
|
||||
this.props.timeZoneOptions,
|
||||
@@ -484,7 +483,7 @@ class AccountSettingsPage extends React.Component {
|
||||
<>
|
||||
<div className="account-section" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
|
||||
{
|
||||
verifiedNameEnabled && this.props.mostRecentVerifiedName
|
||||
this.props.mostRecentVerifiedName
|
||||
&& this.renderVerifiedNameMessage(this.props.mostRecentVerifiedName)
|
||||
}
|
||||
|
||||
@@ -512,8 +511,7 @@ class AccountSettingsPage extends React.Component {
|
||||
name="name"
|
||||
type="text"
|
||||
value={
|
||||
verifiedNameEnabled
|
||||
&& verifiedName?.status === 'submitted'
|
||||
verifiedName?.status === 'submitted'
|
||||
&& this.props.formValues.pending_name_change
|
||||
? this.props.formValues.pending_name_change
|
||||
: this.props.formValues.name
|
||||
@@ -525,22 +523,22 @@ class AccountSettingsPage extends React.Component {
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
helpText={
|
||||
verifiedNameEnabled && verifiedName
|
||||
verifiedName
|
||||
? this.renderFullNameHelpText(verifiedName.status)
|
||||
: this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])
|
||||
}
|
||||
isEditable={
|
||||
verifiedNameEnabled && verifiedName
|
||||
verifiedName
|
||||
? this.isEditable('verifiedName') && this.isEditable('name')
|
||||
: this.isEditable('name')
|
||||
}
|
||||
isGrayedOut={
|
||||
verifiedNameEnabled && verifiedName && !this.isEditable('verifiedName')
|
||||
verifiedName && !this.isEditable('verifiedName')
|
||||
}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitProfileName}
|
||||
/>
|
||||
{verifiedNameEnabled && verifiedName
|
||||
{verifiedName
|
||||
&& (
|
||||
<EditableField
|
||||
name="verified_name"
|
||||
@@ -887,7 +885,6 @@ AccountSettingsPage.propTypes = {
|
||||
nameChangeModal: PropTypes.shape({
|
||||
formId: PropTypes.string,
|
||||
}),
|
||||
verifiedNameEnabled: PropTypes.bool,
|
||||
verifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
@@ -924,7 +921,6 @@ AccountSettingsPage.defaultProps = {
|
||||
isActive: true,
|
||||
secondary_email_enabled: false,
|
||||
nameChangeModal: {},
|
||||
verifiedNameEnabled: false,
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: [],
|
||||
|
||||
@@ -28,9 +28,8 @@ function CertificatePreference({
|
||||
originalVerifiedName,
|
||||
saveState,
|
||||
useVerifiedNameForCerts,
|
||||
verifiedNameEnabled,
|
||||
}) {
|
||||
if (!verifiedNameEnabled || !originalVerifiedName) {
|
||||
if (!originalVerifiedName) {
|
||||
// If the user doesn't have an approved verified name, do not display this component
|
||||
return null;
|
||||
}
|
||||
@@ -161,7 +160,6 @@ CertificatePreference.propTypes = {
|
||||
originalVerifiedName: PropTypes.string,
|
||||
saveState: PropTypes.string,
|
||||
useVerifiedNameForCerts: PropTypes.bool,
|
||||
verifiedNameEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
CertificatePreference.defaultProps = {
|
||||
@@ -169,7 +167,6 @@ CertificatePreference.defaultProps = {
|
||||
originalVerifiedName: '',
|
||||
saveState: null,
|
||||
useVerifiedNameForCerts: false,
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
|
||||
export default connect(certPreferenceSelector)(injectIntl(CertificatePreference));
|
||||
|
||||
@@ -56,7 +56,6 @@ describe('NameChange', () => {
|
||||
originalVerifiedName: 'edX Verified',
|
||||
saveState: null,
|
||||
useVerifiedNameForCerts: false,
|
||||
verifiedNameEnabled: true,
|
||||
intl: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ export const defaultState = {
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: {},
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
|
||||
@@ -12,7 +12,6 @@ const verifiedNameSettingsSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => ({
|
||||
history: accountSettings.verifiedNameHistory.results,
|
||||
verifiedNameEnabled: accountSettings?.verifiedNameHistory.verified_name_enabled,
|
||||
useVerifiedNameForCerts: accountSettings?.verifiedNameHistory.use_verified_name_for_certs,
|
||||
}),
|
||||
);
|
||||
@@ -229,7 +228,6 @@ export const accountSettingsPageSelector = createSelector(
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
sortedVerifiedNameHistorySelector,
|
||||
verifiedNameSettingsSelector,
|
||||
(
|
||||
accountSettings,
|
||||
siteLanguageOptions,
|
||||
@@ -247,7 +245,6 @@ export const accountSettingsPageSelector = createSelector(
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
verifiedNameSettings,
|
||||
) => ({
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
@@ -268,19 +265,16 @@ export const accountSettingsPageSelector = createSelector(
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
verifiedNameEnabled: verifiedNameSettings?.verifiedNameEnabled,
|
||||
}),
|
||||
);
|
||||
|
||||
export const certPreferenceSelector = createSelector(
|
||||
verifiedNameSettingsSelector,
|
||||
valuesSelector,
|
||||
formValuesSelector,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
saveStateSelector,
|
||||
errorSelector,
|
||||
(
|
||||
verifiedNameSettings,
|
||||
committedValues,
|
||||
formValues,
|
||||
mostRecentApprovedVerifiedNameValue,
|
||||
@@ -291,7 +285,6 @@ export const certPreferenceSelector = createSelector(
|
||||
originalVerifiedName: mostRecentApprovedVerifiedNameValue?.verified_name || '',
|
||||
useVerifiedNameForCerts: formValues.useVerifiedNameForCerts || false,
|
||||
saveState,
|
||||
verifiedNameEnabled: verifiedNameSettings.verifiedNameEnabled || false,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -177,19 +177,6 @@ export async function shouldDisplayDemographicsQuestions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getVerifiedNameEnabled() {
|
||||
let data;
|
||||
const client = getAuthenticatedHttpClient();
|
||||
try {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name_enabled`;
|
||||
({ data } = await client.get(requestUrl));
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getVerifiedName() {
|
||||
let data;
|
||||
const client = getAuthenticatedHttpClient();
|
||||
|
||||
@@ -521,21 +521,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'The file you have selected is too large. Please try again with a file less than 10MB.',
|
||||
description: 'Error message for file upload that is larger than 10MB.',
|
||||
},
|
||||
'id.verification.account.name.title': {
|
||||
id: 'id.verification.account.name.title',
|
||||
defaultMessage: 'Account Name Check',
|
||||
description: 'Title for the Account Name Check page.',
|
||||
},
|
||||
'id.verification.name.check.title': {
|
||||
id: 'id.verification.name.check.title',
|
||||
defaultMessage: 'Double-Check Your Name',
|
||||
description: 'Title for the page where a user double-checks that their name is correct.',
|
||||
},
|
||||
'id.verification.account.name.instructions': {
|
||||
id: 'id.verification.account.name.instructions',
|
||||
defaultMessage: 'The name on your account and the name on your ID must be an exact match. If not, please click "No" to update your account name.',
|
||||
description: 'Text to verify that the account name matches the name on the ID photo.',
|
||||
},
|
||||
'id.verification.name.check.instructions': {
|
||||
id: 'id.verification.name.check.instructions',
|
||||
defaultMessage: 'Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.',
|
||||
@@ -546,41 +536,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'If the name below does not match your government-issued ID, your identity verification will be denied.',
|
||||
description: 'Text to inform the user that if the name displayed on the page does not match what is on their government-issued ID, identity verification will be denied.',
|
||||
},
|
||||
'id.verification.account.name.radio.label': {
|
||||
id: 'id.verification.account.name.radio.label',
|
||||
defaultMessage: 'Does the name on your ID match the Account Name below?',
|
||||
description: 'Question to ask the user whether their account name match the name on their ID card.',
|
||||
},
|
||||
'id.verification.name.check.radio.label': {
|
||||
id: 'id.verification.name.check.radio.label',
|
||||
defaultMessage: 'Select an option',
|
||||
description: 'Label for a radio button group where the user needs to choose one of two options.',
|
||||
},
|
||||
'id.verification.account.name.radio.yes': {
|
||||
id: 'id.verification.account.name.radio.yes',
|
||||
defaultMessage: 'Yes',
|
||||
description: 'The radio button that says the account name matches.',
|
||||
},
|
||||
'id.verification.name.check.radio.yes': {
|
||||
id: 'id.verification.name.check.radio.yes',
|
||||
defaultMessage: 'Yes, the name below matches my ID',
|
||||
description: 'Label for a radio button that indicates that the name displayed on the page matches the name on the user\'s ID.',
|
||||
},
|
||||
'id.verification.account.name.radio.no': {
|
||||
id: 'id.verification.account.name.radio.no',
|
||||
defaultMessage: 'No',
|
||||
description: 'The radio button that says the account name does not match.',
|
||||
},
|
||||
'id.verification.name.check.radio.no': {
|
||||
id: 'id.verification.name.check.radio.no',
|
||||
defaultMessage: 'No, the name below does not match my ID',
|
||||
description: 'Label for a radio button that indicates that the name displayed on the page does not match the name on the user\'s ID.',
|
||||
},
|
||||
'id.verification.account.name.error': {
|
||||
id: 'id.verification.account.name.error',
|
||||
defaultMessage: 'Please update account name to match the name on your ID.',
|
||||
description: 'Error that shows when the user needs to update their account name to match the name on their ID.',
|
||||
},
|
||||
'id.verification.name.error': {
|
||||
id: 'id.verification.name.error',
|
||||
defaultMessage: 'Please enter your name as it appears on your government-issued ID.',
|
||||
@@ -596,11 +551,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Account Settings',
|
||||
description: 'Link to Account Settings.',
|
||||
},
|
||||
'id.verification.account.name.label': {
|
||||
id: 'id.verification.account.name.label',
|
||||
defaultMessage: 'Account Name',
|
||||
description: 'Label for account name input.',
|
||||
},
|
||||
'id.verification.name.label': {
|
||||
id: 'id.verification.name.label',
|
||||
defaultMessage: 'Name',
|
||||
@@ -611,11 +561,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Photo of your ID to be submitted.',
|
||||
description: 'Alt text for the photo of the user\'s ID.',
|
||||
},
|
||||
'id.verification.account.name.save': {
|
||||
id: 'id.verification.account.name.save',
|
||||
defaultMessage: 'Save and Next',
|
||||
description: 'Button to save the account name.',
|
||||
},
|
||||
'id.verification.review.title': {
|
||||
id: 'id.verification.review.title',
|
||||
defaultMessage: 'Review Your Photos',
|
||||
|
||||
@@ -15,7 +15,7 @@ import { VerifiedNameContext } from './VerifiedNameContext';
|
||||
|
||||
export default function IdVerificationContextProvider({ children }) {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const { verifiedNameHistoryCallStatus, verifiedName, verifiedNameEnabled } = useContext(VerifiedNameContext);
|
||||
const { verifiedNameHistoryCallStatus, verifiedName } = useContext(VerifiedNameContext);
|
||||
|
||||
const idVerificationData = useAsyncCall(getExistingIdVerification);
|
||||
const enrollmentsData = useAsyncCall(getEnrollments);
|
||||
@@ -64,20 +64,6 @@ export default function IdVerificationContextProvider({ children }) {
|
||||
existingIdVerification = idVerificationData.data;
|
||||
}
|
||||
|
||||
if (verifiedNameHistoryCallStatus === SUCCESS_STATUS && idVerificationData.status === SUCCESS_STATUS) {
|
||||
// With verified name we can redo verification multiple times
|
||||
// if not a successful request prevents re-verification
|
||||
if (!verifiedNameEnabled && existingIdVerification && !existingIdVerification.canVerify) {
|
||||
const { status } = existingIdVerification;
|
||||
canVerify = false;
|
||||
if (status === 'pending' || status === 'approved') {
|
||||
error = ERROR_REASONS.EXISTING_REQUEST;
|
||||
} else {
|
||||
error = ERROR_REASONS.CANNOT_VERIFY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enrollmentsData.status === SUCCESS_STATUS && enrollmentsData?.data) {
|
||||
const verifiedEnrollments = enrollmentsData.data.filter((enrollment) => (
|
||||
VERIFIED_MODES.includes(enrollment.mode)
|
||||
|
||||
@@ -11,22 +11,15 @@ export const VerifiedNameContext = createContext();
|
||||
export function VerifiedNameContextProvider({ children }) {
|
||||
const verifiedNameHistoryData = useAsyncCall(getVerifiedNameHistory);
|
||||
|
||||
let verifiedNameEnabled = false;
|
||||
let verifiedName = '';
|
||||
const { status, data } = verifiedNameHistoryData;
|
||||
if (status === SUCCESS_STATUS && data) {
|
||||
const { verified_name_enabled: verifiedNameFeatureEnabled, results } = data;
|
||||
verifiedNameEnabled = verifiedNameFeatureEnabled;
|
||||
|
||||
if (verifiedNameFeatureEnabled) {
|
||||
const applicableVerifiedName = getMostRecentApprovedOrPendingVerifiedName(results);
|
||||
verifiedName = applicableVerifiedName;
|
||||
}
|
||||
const { results } = data;
|
||||
verifiedName = getMostRecentApprovedOrPendingVerifiedName(results);
|
||||
}
|
||||
|
||||
const value = {
|
||||
verifiedNameHistoryCallStatus: status,
|
||||
verifiedNameEnabled,
|
||||
verifiedName,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import React, {
|
||||
useContext, useState, useEffect, useRef,
|
||||
useContext, useEffect, useRef,
|
||||
} from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Form } from '@edx/paragon';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
import { VerifiedNameContext } from '../VerifiedNameContext';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function GetNameIdPanelVerified(props) {
|
||||
function GetNameIdPanel(props) {
|
||||
const { push, location } = useHistory();
|
||||
const nameInputRef = useRef();
|
||||
const panelSlug = 'get-name-id';
|
||||
@@ -94,180 +91,7 @@ function GetNameIdPanelVerified(props) {
|
||||
);
|
||||
}
|
||||
|
||||
function GetNameIdPanelNonVerified(props) {
|
||||
const { push } = useHistory();
|
||||
const panelSlug = 'get-name-id';
|
||||
const [nameMatches, setNameMatches] = useState(true);
|
||||
const nameInputRef = useRef();
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
|
||||
const {
|
||||
nameOnAccount, userId, profileDataManager, idPhotoName, setIdPhotoName,
|
||||
} = useContext(IdVerificationContext);
|
||||
const nameOnAccountValue = nameOnAccount || '';
|
||||
const invalidName = !nameMatches && (!idPhotoName || idPhotoName === nameOnAccount);
|
||||
const blankName = !nameOnAccount && !idPhotoName;
|
||||
|
||||
useEffect(() => {
|
||||
setIdPhotoName(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nameMatches && nameInputRef.current) {
|
||||
nameInputRef.current.focus();
|
||||
}
|
||||
if (!nameMatches) {
|
||||
sendTrackEvent('edx.id_verification.name_change', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
if (blankName) {
|
||||
setNameMatches(false);
|
||||
}
|
||||
}, [nameMatches, blankName]);
|
||||
|
||||
function getNameValue() {
|
||||
if (!nameMatches) {
|
||||
// Explicitly check for null, as an empty string should still be used here
|
||||
if (idPhotoName === null) {
|
||||
return nameOnAccountValue;
|
||||
}
|
||||
return idPhotoName;
|
||||
}
|
||||
return nameOnAccountValue;
|
||||
}
|
||||
|
||||
function getErrorMessage() {
|
||||
if (profileDataManager) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.managed.alert"
|
||||
defaultMessage="Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help."
|
||||
description="Alert message informing the user their account name is managed by a third party."
|
||||
values={{
|
||||
managerTitle: <strong>{profileDataManager}</strong>,
|
||||
profileDataManager,
|
||||
support: (
|
||||
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
|
||||
{props.intl.formatMessage(messages['id.verification.support'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return props.intl.formatMessage(messages['id.verification.account.name.error']);
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
// If the input is empty, or if no changes have been made to the
|
||||
// mismatching name, the user should not be able to proceed.
|
||||
if (!invalidName && !blankName) {
|
||||
push(nextPanelSlug);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.account.name.title'])}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.instructions'])}
|
||||
</p>
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Label className="font-weight-bold" htmlFor="nameMatchesYes">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.radio.label'])}
|
||||
</Form.Label>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="nameMatchesYes"
|
||||
name="nameMatches"
|
||||
data-testid="name-matches-yes"
|
||||
label={props.intl.formatMessage(messages['id.verification.account.name.radio.yes'])}
|
||||
checked={nameMatches}
|
||||
disabled={!nameOnAccount}
|
||||
onChange={() => {
|
||||
setNameMatches(true);
|
||||
setIdPhotoName(null);
|
||||
}}
|
||||
/>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="nameMatchesNo"
|
||||
name="nameMatches"
|
||||
data-testid="name-matches-no"
|
||||
label={props.intl.formatMessage(messages['id.verification.account.name.radio.no'])}
|
||||
checked={!nameMatches}
|
||||
disabled={!nameOnAccount}
|
||||
onChange={() => setNameMatches(false)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label className="font-weight-bold" htmlFor="photo-id-name">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
controlId="photo-id-name"
|
||||
size="lg"
|
||||
type="text"
|
||||
ref={nameInputRef}
|
||||
readOnly={nameMatches || !!profileDataManager}
|
||||
isInvalid={invalidName || blankName}
|
||||
aria-describedby="photo-id-name-feedback"
|
||||
value={getNameValue()}
|
||||
onChange={e => setIdPhotoName(e.target.value)}
|
||||
data-testid="name-input"
|
||||
/>
|
||||
{(invalidName || !!profileDataManager) && (
|
||||
<Form.Control.Feedback
|
||||
id="photo-id-name-feedback"
|
||||
data-testid="id-name-feedback-message"
|
||||
type="invalid"
|
||||
>
|
||||
{getErrorMessage()}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
||||
<div className="action-row">
|
||||
<Link
|
||||
to={nextPanelSlug}
|
||||
className={`btn btn-primary ${(invalidName || blankName) && 'disabled'}`}
|
||||
data-testid="next-button"
|
||||
aria-disabled={invalidName || blankName}
|
||||
>
|
||||
{
|
||||
!nameMatches
|
||||
? props.intl.formatMessage(messages['id.verification.account.name.save'])
|
||||
: props.intl.formatMessage(messages['id.verification.next'])
|
||||
}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
function GetNameIdPanel(props) {
|
||||
const { verifiedNameEnabled } = useContext(VerifiedNameContext);
|
||||
|
||||
if (verifiedNameEnabled) {
|
||||
return <GetNameIdPanelVerified {...props} />;
|
||||
}
|
||||
|
||||
return <GetNameIdPanelNonVerified {...props} />;
|
||||
}
|
||||
|
||||
GetNameIdPanelVerified.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
GetNameIdPanelNonVerified.propTypes = {
|
||||
GetNameIdPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
import ImagePreview from '../ImagePreview';
|
||||
import { VerifiedNameContext } from '../VerifiedNameContext';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
import CameraHelpWithUpload from '../CameraHelpWithUpload';
|
||||
@@ -32,7 +31,6 @@ function SummaryPanel(props) {
|
||||
portraitPhotoMode,
|
||||
idPhotoMode,
|
||||
} = useContext(IdVerificationContext);
|
||||
const { verifiedNameEnabled } = useContext(VerifiedNameContext);
|
||||
const nameToBeUsed = idPhotoName || nameOnAccount || '';
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submissionError, setSubmissionError] = useState(null);
|
||||
@@ -74,17 +72,10 @@ function SummaryPanel(props) {
|
||||
};
|
||||
if (idPhotoName) {
|
||||
verificationData.idPhotoName = idPhotoName;
|
||||
} else if (verifiedNameEnabled) {
|
||||
} else {
|
||||
/**
|
||||
* If learner has not entered an idPhotoName on the GetNameIdPanel,
|
||||
* and the verified name feature is enabled, use the current nameOnAccount
|
||||
* when submitting IDV. The reason we only do this if the feature is enabled
|
||||
* is that, when the feature is off, the server will change the learner's
|
||||
* profile name to this value. If we send the idPhotoName on all requests,
|
||||
* even ones where the learner does not change the idPhotoName, then the
|
||||
* server will record that the full name on the learner's profile has
|
||||
* a requested change, even if the name is the same. This will pollute
|
||||
* the history.
|
||||
* use the current nameOnAccount when submitting IDV.
|
||||
*/
|
||||
verificationData.idPhotoName = nameOnAccount;
|
||||
}
|
||||
@@ -220,9 +211,7 @@ function SummaryPanel(props) {
|
||||
{!optimizelyExperimentName && <CameraHelpWithUpload />}
|
||||
<div className="form-group">
|
||||
<label htmlFor="name-to-be-used" className="font-weight-bold">
|
||||
{verifiedNameEnabled
|
||||
? props.intl.formatMessage(messages['id.verification.name.label'])
|
||||
: props.intl.formatMessage(messages['id.verification.account.name.label'])}
|
||||
{props.intl.formatMessage(messages['id.verification.name.label'])}
|
||||
</label>
|
||||
{renderManagedProfileMessage()}
|
||||
<div className="d-flex">
|
||||
@@ -242,29 +231,14 @@ function SummaryPanel(props) {
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
>
|
||||
{
|
||||
verifiedNameEnabled
|
||||
? (
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.edit"
|
||||
defaultMessage="Edit {sr}"
|
||||
description="Button to edit account name, with clarifying information for screen readers."
|
||||
values={{
|
||||
sr: <span className="sr-only">Name</span>,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.edit"
|
||||
defaultMessage="Edit {sr}"
|
||||
description="Button to edit account name, with clarifying information for screen readers."
|
||||
values={{
|
||||
sr: <span className="sr-only">Account Name</span>,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.edit"
|
||||
defaultMessage="Edit {sr}"
|
||||
description="Button to edit name, with clarifying information for screen readers."
|
||||
values={{
|
||||
sr: <span className="sr-only">Name</span>,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('IdVerificationContextProvider', () => {
|
||||
|
||||
it('renders correctly and calls getExistingIdVerification + getEnrollments', async () => {
|
||||
const appContext = { authenticatedUser: { userId: 3, roles: [] } };
|
||||
const verifiedNameContext = { verifiedName: '', verifiedNameEnabled: false };
|
||||
const verifiedNameContext = { verifiedName: '' };
|
||||
await act(async () => render((
|
||||
<AppContext.Provider value={appContext}>
|
||||
<VerifiedNameContext.Provider value={verifiedNameContext}>
|
||||
@@ -54,7 +54,7 @@ describe('IdVerificationContextProvider', () => {
|
||||
roles: ['enterprise_learner'],
|
||||
},
|
||||
};
|
||||
const verifiedNameContext = { verifiedName: '', verifiedNameEnabled: false };
|
||||
const verifiedNameContext = { verifiedName: '' };
|
||||
await act(async () => render((
|
||||
<AppContext.Provider value={appContext}>
|
||||
<VerifiedNameContext.Provider value={verifiedNameContext}>
|
||||
|
||||
@@ -5,11 +5,10 @@ import { getVerifiedNameHistory } from '../../account-settings/data/service';
|
||||
import { VerifiedNameContext, VerifiedNameContextProvider } from '../VerifiedNameContext';
|
||||
|
||||
const VerifiedNameContextTestComponent = () => {
|
||||
const { verifiedName, verifiedNameEnabled } = useContext(VerifiedNameContext);
|
||||
const { verifiedName } = useContext(VerifiedNameContext);
|
||||
return (
|
||||
<>
|
||||
{verifiedNameEnabled && (<div data-testid="verified-name">{verifiedName}</div>)}
|
||||
<div data-testid="verified-name-enabled">{verifiedNameEnabled ? 'true' : 'false'}</div>
|
||||
{verifiedName && (<div data-testid="verified-name">{verifiedName}</div>)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -38,9 +37,8 @@ describe('VerifiedNameContextProvider', () => {
|
||||
await waitFor(() => expect(getVerifiedNameHistory).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('sets verifiedName and verifiedNameEnabled correctly when verified name feature enabled', async () => {
|
||||
it('sets verifiedName', async () => {
|
||||
const mockReturnValue = {
|
||||
verified_name_enabled: true,
|
||||
results: [{
|
||||
verified_name: 'Michael',
|
||||
status: 'approved',
|
||||
@@ -57,28 +55,5 @@ describe('VerifiedNameContextProvider', () => {
|
||||
|
||||
await waitFor(() => expect(getVerifiedNameHistory).toHaveBeenCalledTimes(1));
|
||||
expect(getByTestId('verified-name')).toHaveTextContent('Michael');
|
||||
expect(getByTestId('verified-name-enabled')).toHaveTextContent('true');
|
||||
});
|
||||
|
||||
it('sets verifiedName and verifiedNameEnabled correctly when verified name feature not enabled', async () => {
|
||||
const mockReturnValue = {
|
||||
verified_name_enabled: false,
|
||||
results: [{
|
||||
verified_name: 'Michael',
|
||||
status: 'approved',
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
}],
|
||||
};
|
||||
getVerifiedNameHistory.mockReturnValueOnce(mockReturnValue);
|
||||
|
||||
const { queryByTestId } = render((
|
||||
<VerifiedNameContextProvider {...defaultProps}>
|
||||
<VerifiedNameContextTestComponent />
|
||||
</VerifiedNameContextProvider>
|
||||
));
|
||||
|
||||
await waitFor(() => expect(getVerifiedNameHistory).toHaveBeenCalledTimes(1));
|
||||
expect(queryByTestId('verified-name')).toBeNull();
|
||||
expect(queryByTestId('verified-name-enabled')).toHaveTextContent('false');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,16 +32,14 @@ describe('GetNameIdPanel', () => {
|
||||
idPhotoFile: 'test.jpg',
|
||||
};
|
||||
|
||||
const verifiedNameContextValue = {
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
const verifiedNameContextValue = {};
|
||||
|
||||
const getPanel = async () => {
|
||||
const getPanel = async (idVerificationContextValue = IDVerificationContextValue) => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<VerifiedNameContext.Provider value={verifiedNameContextValue}>
|
||||
<IdVerificationContext.Provider value={IDVerificationContextValue}>
|
||||
<IdVerificationContext.Provider value={idVerificationContextValue}>
|
||||
<IntlGetNameIdPanel {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</VerifiedNameContext.Provider>
|
||||
@@ -54,65 +52,28 @@ describe('GetNameIdPanel', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('edits', async () => {
|
||||
it('shows feedback message when user has an empty name', async () => {
|
||||
await getPanel();
|
||||
|
||||
const yesButton = await screen.findByTestId('name-matches-yes');
|
||||
const noButton = await screen.findByTestId('name-matches-no');
|
||||
const input = await screen.findByTestId('name-input');
|
||||
const nextButton = await screen.findByTestId('next-button');
|
||||
const errorMessageQuery = await screen.queryByTestId('id-name-feedback-message');
|
||||
|
||||
expect(input).toHaveAttribute('readonly');
|
||||
expect(errorMessageQuery).toBeNull();
|
||||
|
||||
fireEvent.click(noButton);
|
||||
expect(input).not.toHaveAttribute('readonly');
|
||||
expect(nextButton.classList.contains('disabled')).toBe(true);
|
||||
expect(nextButton).toHaveAttribute('aria-disabled');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'test change' } });
|
||||
expect(IDVerificationContextValue.setIdPhotoName).toHaveBeenCalled();
|
||||
// Ensure the feedback message on name shows when the user says the name does not match ID
|
||||
// Ensure the feedback message on name shows when the user has an empty name
|
||||
expect(await screen.queryByTestId('id-name-feedback-message')).toBeTruthy();
|
||||
|
||||
fireEvent.click(yesButton);
|
||||
expect(input).toHaveAttribute('readonly');
|
||||
expect(IDVerificationContextValue.setIdPhotoName).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables radio buttons + next button and enables input if account name is blank', async () => {
|
||||
IDVerificationContextValue.nameOnAccount = '';
|
||||
await getPanel();
|
||||
|
||||
const yesButton = await screen.findByTestId('name-matches-yes');
|
||||
const noButton = await screen.findByTestId('name-matches-no');
|
||||
const input = await screen.findByTestId('name-input');
|
||||
const nextButton = await screen.findByTestId('next-button');
|
||||
const errorMessageQuery = await screen.queryByTestId('id-name-feedback-message');
|
||||
|
||||
expect(yesButton).toBeDisabled();
|
||||
expect(noButton).toBeDisabled();
|
||||
expect(input).not.toHaveAttribute('readonly');
|
||||
expect(nextButton.classList.contains('disabled')).toBe(true);
|
||||
expect(nextButton).toHaveAttribute('aria-disabled');
|
||||
expect(errorMessageQuery).toBeTruthy();
|
||||
it('does not show feedback message when user has an non-empty name', async () => {
|
||||
const idVerificationContextValue = {
|
||||
...IDVerificationContextValue,
|
||||
idPhotoName: 'test',
|
||||
};
|
||||
await getPanel(idVerificationContextValue);
|
||||
// Ensure the feedback message on name shows when the user has an empty name
|
||||
expect(await screen.queryByTestId('id-name-feedback-message')).toBeNull();
|
||||
});
|
||||
|
||||
it('blocks the user from changing account name if managed by a third party', async () => {
|
||||
IDVerificationContextValue.profileDataManager = 'test-org';
|
||||
it('calls setIdPhotoName with correct name', async () => {
|
||||
await getPanel();
|
||||
|
||||
const noButton = await screen.findByTestId('name-matches-no');
|
||||
const input = await screen.findByTestId('name-input');
|
||||
const nextButton = await screen.findByTestId('next-button');
|
||||
|
||||
fireEvent.click(noButton);
|
||||
expect(input).toHaveAttribute('readonly');
|
||||
expect(nextButton.classList.contains('disabled')).toBe(true);
|
||||
expect(nextButton).toHaveAttribute('aria-disabled');
|
||||
const warning = await screen.getAllByText('test-org');
|
||||
expect(warning.length).toEqual(1);
|
||||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
expect(IDVerificationContextValue.setIdPhotoName).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
it('routes to SummaryPanel', async () => {
|
||||
|
||||
@@ -39,9 +39,7 @@ describe('SummaryPanel', () => {
|
||||
setReachedSummary: jest.fn(),
|
||||
};
|
||||
|
||||
const verifiedNameContextValue = {
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
const verifiedNameContextValue = {};
|
||||
|
||||
const getPanel = async () => {
|
||||
await act(async () => render((
|
||||
@@ -111,7 +109,7 @@ describe('SummaryPanel', () => {
|
||||
await waitFor(() => expect(appContextValue.stopUserMedia).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('does not submit a name if name is blank', async () => {
|
||||
it('submits a name if name is blank', async () => {
|
||||
appContextValue.idPhotoName = '';
|
||||
const verificationData = {
|
||||
facePhotoFile: appContextValue.facePhotoFile,
|
||||
@@ -120,6 +118,7 @@ describe('SummaryPanel', () => {
|
||||
idPhotoMode: appContextValue.idPhotoMode,
|
||||
optimizelyExperimentName: appContextValue.optimizelyExperimentName,
|
||||
courseRunKey: null,
|
||||
idPhotoName: appContextValue.nameOnAccount,
|
||||
};
|
||||
await getPanel();
|
||||
const button = await screen.findByTestId('submit-button');
|
||||
@@ -127,25 +126,8 @@ describe('SummaryPanel', () => {
|
||||
expect(dataService.submitIdVerification).toHaveBeenCalledWith(verificationData);
|
||||
});
|
||||
|
||||
it('does not submit a name if name is unchanged', async () => {
|
||||
it('submits a name if a name is unchanged', async () => {
|
||||
appContextValue.idPhotoName = null;
|
||||
const verificationData = {
|
||||
facePhotoFile: appContextValue.facePhotoFile,
|
||||
idPhotoFile: appContextValue.idPhotoFile,
|
||||
portraitPhotoMode: appContextValue.portraitPhotoMode,
|
||||
idPhotoMode: appContextValue.idPhotoMode,
|
||||
optimizelyExperimentName: appContextValue.optimizelyExperimentName,
|
||||
courseRunKey: null,
|
||||
};
|
||||
await getPanel();
|
||||
const button = await screen.findByTestId('submit-button');
|
||||
fireEvent.click(button);
|
||||
expect(dataService.submitIdVerification).toHaveBeenCalledWith(verificationData);
|
||||
});
|
||||
|
||||
it('submits a name if a name is unchanged if verified name feature is enabled', async () => {
|
||||
appContextValue.idPhotoName = null;
|
||||
verifiedNameContextValue.verifiedNameEnabled = true;
|
||||
const verificationData = {
|
||||
facePhotoFile: appContextValue.facePhotoFile,
|
||||
idPhotoFile: appContextValue.idPhotoFile,
|
||||
|
||||
Reference in New Issue
Block a user