Merge pull request #289 from edx/bseverino/idv-improvements
Add i18n and UX improvements to IDV
This commit is contained in:
@@ -67,7 +67,11 @@ class Camera extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-primary camera-btn'
|
||||
className={`btn camera-btn ${
|
||||
this.state.dataUri ?
|
||||
'btn-outline-primary'
|
||||
: 'btn-primary'
|
||||
}`}
|
||||
accessKey='c'
|
||||
onClick={() => {
|
||||
this.takePhoto();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
@@ -10,13 +10,22 @@ function ExistingRequest(props) {
|
||||
<h3 aria-level="1" tabIndex="-1">
|
||||
{props.intl.formatMessage(messages['id.verification.existing.request.title'])}
|
||||
</h3>
|
||||
{props.status === 'pending' || props.status == 'approved'
|
||||
{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>
|
||||
: <FormattedMessage
|
||||
id="id.verification.existing.request.denied.text"
|
||||
defaultMessage="You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}."
|
||||
description="Text that displays when user is denied from making a request, and to check their email for an activation email."
|
||||
values={{
|
||||
email: <strong>no-reply@registration.edx.org</strong>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{props.intl.formatMessage(messages['id.verification.return'])}
|
||||
</a>
|
||||
<div className="action-row">
|
||||
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{props.intl.formatMessage(messages['id.verification.return.dashboard'])}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,14 +68,9 @@ const messages = defineMessages({
|
||||
},
|
||||
'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).',
|
||||
defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).',
|
||||
description: 'Text that displays when user has a pending or approved request.',
|
||||
},
|
||||
'id.verification.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',
|
||||
@@ -91,6 +86,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Camera Permissions',
|
||||
description: 'Title for the Camera Access page.',
|
||||
},
|
||||
'id.verification.camera.access.title.success': {
|
||||
id: 'id.verification.camera.access.title.success',
|
||||
defaultMessage: 'Camera Access Enabled',
|
||||
description: 'Title for the Camera Access page when camera is enabled.',
|
||||
},
|
||||
'id.verification.camera.access.title.failed': {
|
||||
id: 'id.verification.camera.access.title.failed',
|
||||
defaultMessage: 'Camera Access Failed',
|
||||
description: 'Title for the Camera Access page when camera access is denied or unavailable.',
|
||||
},
|
||||
'id.verification.camera.access.click.allow': {
|
||||
id: 'id.verification.camera.access.click.allow',
|
||||
defaultMessage: 'Please make sure to click "Allow"',
|
||||
@@ -158,12 +163,12 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.portrait.photo.title.camera': {
|
||||
id: 'id.verification.portrait.photo.title.camera',
|
||||
defaultMessage: 'Take Your Photo',
|
||||
defaultMessage: 'Take a Photo of Yourself',
|
||||
description: 'Title for the Portrait Photo page if camera access is enabled.',
|
||||
},
|
||||
'id.verification.portrait.photo.title.upload': {
|
||||
id: 'id.verification.portrait.photo.title.upload',
|
||||
defaultMessage: 'Upload Your Portrait Photo',
|
||||
defaultMessage: 'Upload a Photo of Yourself',
|
||||
description: 'Title for the Portrait Photo page if camera access is disabled.',
|
||||
},
|
||||
'id.verification.portrait.photo.preview.alt': {
|
||||
@@ -208,7 +213,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.id.tips.description': {
|
||||
id: 'id.verification.id.tips.description',
|
||||
defaultMessage: 'Next you\'ll need an eligible ID photo, make sure that:',
|
||||
defaultMessage: 'Next, we\'ll need you to take a photo of a valid ID that includes your name. Please have your ID ready. Make sure that:',
|
||||
description: 'Description for the ID Tips page.',
|
||||
},
|
||||
'id.verification.id.tips.list.well.lit': {
|
||||
@@ -223,12 +228,12 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.id.photo.title.camera': {
|
||||
id: 'id.verification.id.photo.title.camera',
|
||||
defaultMessage: 'Take ID Photo',
|
||||
defaultMessage: 'Take a Photo of Your ID',
|
||||
description: 'Title for the ID Photo page if camera access is enabled.',
|
||||
},
|
||||
'id.verification.id.photo.title.upload': {
|
||||
id: 'id.verification.id.photo.title.upload',
|
||||
defaultMessage: 'Upload Your ID Photo',
|
||||
defaultMessage: 'Upload a Photo of Your ID',
|
||||
description: 'Title for the ID Photo page if camera access is disabled.',
|
||||
},
|
||||
'id.verification.id.photo.preview.alt': {
|
||||
@@ -253,9 +258,29 @@ const messages = defineMessages({
|
||||
},
|
||||
'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".',
|
||||
defaultMessage: 'The name on your account and the name on your ID must be an exact match. If not, please click "No" to update your account name.',
|
||||
description: 'Text to verify that the account name matches the name on the ID photo.',
|
||||
},
|
||||
'id.verification.account.name.radio.label': {
|
||||
id: 'id.verification.account.name.radio.label',
|
||||
defaultMessage: 'Does the name on your ID match the Account Name below?',
|
||||
description: 'Question to ask the user whether their account name match the name on their ID card.',
|
||||
},
|
||||
'id.verification.account.name.radio.yes': {
|
||||
id: 'id.verification.account.name.radio.yes',
|
||||
defaultMessage: 'Yes',
|
||||
description: 'The radio button that says the account name matches.',
|
||||
},
|
||||
'id.verification.account.name.radio.no': {
|
||||
id: 'id.verification.account.name.radio.no',
|
||||
defaultMessage: 'No',
|
||||
description: 'The radio button that says the account name does not match.',
|
||||
},
|
||||
'id.verification.account.name.error': {
|
||||
id: 'id.verification.account.name.error',
|
||||
defaultMessage: 'Please update account name to match the name on your ID.',
|
||||
description: 'Error that shows when the user needs to update their account name to match the name on their ID.',
|
||||
},
|
||||
'id.verification.account.name.warning.prefix': {
|
||||
id: 'id.verification.account.name.warning.prefix',
|
||||
defaultMessage: 'Please Note:',
|
||||
@@ -268,7 +293,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.account.name.label': {
|
||||
id: 'id.verification.account.name.label',
|
||||
defaultMessage: 'Name on ID',
|
||||
defaultMessage: 'Account Name',
|
||||
description: 'Label for account name input.',
|
||||
},
|
||||
'id.verification.account.name.edit': {
|
||||
@@ -283,7 +308,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.account.name.save': {
|
||||
id: 'id.verification.account.name.save',
|
||||
defaultMessage: 'Save',
|
||||
defaultMessage: 'Save and Next',
|
||||
description: 'Button to save the account name.',
|
||||
},
|
||||
'id.verification.review.title': {
|
||||
@@ -328,8 +353,8 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.review.confirm': {
|
||||
id: 'id.verification.review.confirm',
|
||||
defaultMessage: 'Confirm',
|
||||
description: 'Button to confirm all information is correct.',
|
||||
defaultMessage: 'Submit',
|
||||
description: 'Button to confirm all information is correct and submit.',
|
||||
},
|
||||
'id.verification.submitted.title': {
|
||||
id: 'id.verification.submitted.title',
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
max-height: 10rem;
|
||||
}
|
||||
}
|
||||
.form-check {
|
||||
padding: 0.5rem 0.5rem 1rem;
|
||||
.form-check-label {
|
||||
margin-left: 0.5rem;
|
||||
padding-top: 0.2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import React, { useContext, useState, useEffect, useRef } from 'react';
|
||||
import { Input, Button } from '@edx/paragon';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Link } 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 ImagePreview from '../ImagePreview';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function GetNameIdPanel(props) {
|
||||
const panelSlug = 'get-name-id';
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [nameMatches, setNameMatches] = useState(true);
|
||||
const nameInputRef = useRef();
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && nameInputRef.current) {
|
||||
nameInputRef.current.focus();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const {
|
||||
nameOnAccount, userId, idPhotoName, setIdPhotoName, idPhotoFile,
|
||||
nameOnAccount, userId, idPhotoName, setIdPhotoName,
|
||||
} = useContext(IdVerificationContext);
|
||||
const nameOnAccountValue = nameOnAccount || '';
|
||||
const invalidName = !nameMatches && (!idPhotoName || idPhotoName === nameOnAccount);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsEditing(true);
|
||||
sendTrackEvent('edx.id_verification.name_change', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
setIdPhotoName('');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nameMatches && nameInputRef.current) {
|
||||
nameInputRef.current.focus();
|
||||
}
|
||||
if (!nameMatches) {
|
||||
sendTrackEvent('edx.id_verification.name_change', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
}, [nameMatches]);
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
@@ -45,53 +47,75 @@ function GetNameIdPanel(props) {
|
||||
{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"
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Label htmlFor="nameMatchesYes">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.radio.label'])}
|
||||
</Form.Label>
|
||||
<Form.Row>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="nameMatchesYes"
|
||||
name="nameMatches"
|
||||
data-testid="name-matches-yes"
|
||||
label={props.intl.formatMessage(messages['id.verification.account.name.radio.yes'])}
|
||||
checked={nameMatches}
|
||||
inline
|
||||
onChange={() => {
|
||||
setNameMatches(true);
|
||||
setIdPhotoName('');
|
||||
}}
|
||||
/>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="nameMatchesNo"
|
||||
name="nameMatches"
|
||||
data-testid="name-matches-no"
|
||||
label={props.intl.formatMessage(messages['id.verification.account.name.radio.no'])}
|
||||
inline
|
||||
checked={!nameMatches}
|
||||
onChange={() => setNameMatches(false)}
|
||||
/>
|
||||
</Form.Row>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label htmlFor="photo-id-name">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
controlId="photo-id-name"
|
||||
size="lg"
|
||||
type="text"
|
||||
ref={nameInputRef}
|
||||
disabled={!isEditing}
|
||||
readOnly={!isEditing}
|
||||
value={idPhotoName || nameOnAccountValue}
|
||||
readOnly={nameMatches}
|
||||
isInvalid={invalidName}
|
||||
aria-describedby="photo-id-name-feedback"
|
||||
value={
|
||||
!nameMatches ?
|
||||
idPhotoName || nameOnAccountValue
|
||||
: nameOnAccountValue
|
||||
}
|
||||
onChange={e => setIdPhotoName(e.target.value)}
|
||||
data-testid="name-input"
|
||||
/>
|
||||
{!isEditing && (
|
||||
<Button
|
||||
className="btn-link px-0 ml-3"
|
||||
onClick={handleClick}
|
||||
data-testid="edit-button"
|
||||
>
|
||||
{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'])}
|
||||
/>
|
||||
<Form.Control.Feedback id="photo-id-name-feedback" type="invalid">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.error'])}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{isEditing ? props.intl.formatMessage(messages['id.verification.account.name.save']) : props.intl.formatMessage(messages['id.verification.next'])}
|
||||
<Link
|
||||
to={nextPanelSlug}
|
||||
className={`btn btn-primary ${invalidName && 'disabled'}`}
|
||||
data-testid="next-button"
|
||||
aria-disabled={invalidName}
|
||||
>
|
||||
{
|
||||
!nameMatches ?
|
||||
props.intl.formatMessage(messages['id.verification.account.name.save'])
|
||||
: props.intl.formatMessage(messages['id.verification.next'])
|
||||
}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
|
||||
@@ -41,10 +41,19 @@ function RequestCameraAccessPanel(props) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTitle = () => {
|
||||
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
|
||||
return props.intl.formatMessage(messages['id.verification.camera.access.title.success']);
|
||||
} else if ([MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess)) {
|
||||
return props.intl.formatMessage(messages['id.verification.camera.access.title.failed']);
|
||||
}
|
||||
return props.intl.formatMessage(messages['id.verification.camera.access.title']);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.camera.access.title'])}
|
||||
title={getTitle()}
|
||||
>
|
||||
{mediaAccess === MEDIA_ACCESS.PENDING && (
|
||||
<div>
|
||||
|
||||
@@ -32,7 +32,7 @@ function ReviewRequirementsPanel(props) {
|
||||
</p>
|
||||
<div className="card mb-4 shadow requirements">
|
||||
<div className="card-body">
|
||||
<h6>
|
||||
<h6 aria-level="3">
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.card.device.title'])}
|
||||
</h6>
|
||||
<p className="mb-0">
|
||||
@@ -49,7 +49,7 @@ function ReviewRequirementsPanel(props) {
|
||||
</div>
|
||||
<div className="card mb-4 shadow requirements">
|
||||
<div className="card-body">
|
||||
<h6>
|
||||
<h6 aria-level="3">
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.card.id.title'])}
|
||||
</h6>
|
||||
<p className="mb-0">
|
||||
@@ -57,16 +57,16 @@ function ReviewRequirementsPanel(props) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="mb-3">
|
||||
<h4 aria-level="2" className="mb-3">
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
|
||||
</h4>
|
||||
<h6>
|
||||
<h6 aria-level="3">
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}
|
||||
</p>
|
||||
<h6>
|
||||
<h6 aria-level="3">
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}
|
||||
</h6>
|
||||
<p>
|
||||
|
||||
@@ -70,7 +70,7 @@ function SummaryPanel(props) {
|
||||
alt={props.intl.formatMessage(messages['id.verification.review.portrait.alt'])}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-inverse-primary shadow"
|
||||
className="btn btn-outline-primary"
|
||||
to={{
|
||||
pathname: 'take-portrait-photo',
|
||||
state: { fromSummary: true },
|
||||
@@ -89,7 +89,7 @@ function SummaryPanel(props) {
|
||||
alt={props.intl.formatMessage(messages['id.verification.review.id.alt'])}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-inverse-primary shadow"
|
||||
className="btn btn-outline-primary"
|
||||
to={{
|
||||
pathname: 'take-id-photo',
|
||||
state: { fromSummary: true },
|
||||
|
||||
@@ -44,12 +44,12 @@ function TakeIdPhotoPanel(props) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{shouldUseCamera && <CameraHelp />}
|
||||
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
{shouldUseCamera && <CameraHelp />}
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,12 +44,12 @@ function TakePortraitPhotoPanel(props) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{shouldUseCamera && <CameraHelp />}
|
||||
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
{shouldUseCamera && <CameraHelp />}
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,13 +43,17 @@ describe('GetNameIdPanel', () => {
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
const button = await screen.findByTestId('edit-button');
|
||||
const yesButton = await screen.findByTestId('name-matches-yes');
|
||||
const noButton = await screen.findByTestId('name-matches-no');
|
||||
const input = await screen.findByTestId('name-input');
|
||||
expect(input).toHaveProperty('disabled', true);
|
||||
fireEvent.click(button);
|
||||
expect(input).toHaveProperty('disabled', false);
|
||||
expect(input).toHaveProperty('readOnly', true);
|
||||
fireEvent.click(noButton);
|
||||
expect(input).toHaveProperty('readOnly', false);
|
||||
fireEvent.change(input, { target: { value: 'test change' } });
|
||||
expect(contextValue.setIdPhotoName).toHaveBeenCalled();
|
||||
fireEvent.click(yesButton);
|
||||
expect(input).toHaveProperty('readOnly', true);
|
||||
expect(contextValue.setIdPhotoName).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes to SummaryPanel', async () => {
|
||||
|
||||
Reference in New Issue
Block a user