Revert "Revert "Prevent IDV access from audit courses""
This commit is contained in:
48
src/id-verification/AccessBlocked.jsx
Normal file
48
src/id-verification/AccessBlocked.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './IdVerification.messages';
|
||||
import { ERROR_REASONS } from './IdVerificationContext';
|
||||
|
||||
function AccessBlocked({ error, intl }) {
|
||||
const handleMessage = () => {
|
||||
if (error === ERROR_REASONS.COURSE_ENROLLMENT) {
|
||||
return <p>{intl.formatMessage(messages['id.verification.access.blocked.enrollment'])}</p>;
|
||||
} else if (error === ERROR_REASONS.EXISTING_REQUEST) {
|
||||
return <p>{intl.formatMessage(messages['id.verification.access.blocked.pending'])}</p>;
|
||||
}
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="id.verification.access.blocked.denied"
|
||||
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>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 aria-level="1" tabIndex="-1">
|
||||
{intl.formatMessage(messages['id.verification.access.blocked.title'])}
|
||||
</h3>
|
||||
{handleMessage()}
|
||||
<div className="action-row">
|
||||
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{intl.formatMessage(messages['id.verification.return.dashboard'])}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AccessBlocked.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AccessBlocked);
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape, FormattedMessage } 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>
|
||||
: <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>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
ExistingRequest.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ExistingRequest);
|
||||
@@ -61,13 +61,18 @@ const messages = defineMessages({
|
||||
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',
|
||||
'id.verification.access.blocked.title': {
|
||||
id: 'id.verification.access.blocked.title',
|
||||
defaultMessage: 'Identity Verification',
|
||||
description: 'Title for text that displays when user has already made a request.',
|
||||
description: 'Title for text that displays when a user is blocked from ID verification.',
|
||||
},
|
||||
'id.verification.existing.request.pending.text': {
|
||||
id: 'id.verification.existing.request.pending.text',
|
||||
'id.verification.access.blocked.enrollment': {
|
||||
id: 'id.verification.access.blocked.enrollment',
|
||||
defaultMessage: 'You are not currently enrolled in a course that requires identity verification.',
|
||||
description: 'Text that displays when user is trying to verify while not enrolled in a course that requires ID verification.',
|
||||
},
|
||||
'id.verification.access.blocked.pending': {
|
||||
id: 'id.verification.access.blocked.pending',
|
||||
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.',
|
||||
},
|
||||
|
||||
@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { hasGetUserMediaSupport } from './getUserMediaShim';
|
||||
import { getExistingIdVerification } from './data/service';
|
||||
import { getExistingIdVerification, getEnrollments } from './data/service';
|
||||
import PageLoading from '../account-settings/PageLoading';
|
||||
import ExistingRequest from './ExistingRequest';
|
||||
import AccessBlocked from './AccessBlocked';
|
||||
|
||||
const IdVerificationContext = React.createContext({});
|
||||
|
||||
@@ -16,6 +16,14 @@ const MEDIA_ACCESS = {
|
||||
GRANTED: 'granted',
|
||||
};
|
||||
|
||||
const ERROR_REASONS = {
|
||||
COURSE_ENROLLMENT: 'course_enrollment',
|
||||
EXISTING_REQUEST: 'existing_request',
|
||||
CANNOT_VERIFY: 'cannot_verify',
|
||||
};
|
||||
|
||||
const VERIFIED_MODES = ['verified', 'professional', 'masters', 'executive_education'];
|
||||
|
||||
function IdVerificationContextProvider({ children }) {
|
||||
const [existingIdVerification, setExistingIdVerification] = useState(null);
|
||||
const [facePhotoFile, setFacePhotoFile] = useState(null);
|
||||
@@ -25,6 +33,8 @@ function IdVerificationContextProvider({ children }) {
|
||||
const [mediaAccess, setMediaAccess] = useState(hasGetUserMediaSupport ?
|
||||
MEDIA_ACCESS.PENDING :
|
||||
MEDIA_ACCESS.UNSUPPORTED);
|
||||
const [canVerify, setCanVerify] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
|
||||
const contextValue = {
|
||||
@@ -61,24 +71,49 @@ function IdVerificationContextProvider({ children }) {
|
||||
},
|
||||
};
|
||||
|
||||
// Call verification status endpoint to check whether we can verify.
|
||||
useEffect(() => {
|
||||
// Call verification status endpoint to check whether we can verify.
|
||||
(async () => {
|
||||
const existingIdV = await getExistingIdVerification();
|
||||
setExistingIdVerification(existingIdV);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Check whether the learner is enrolled in a verified course mode.
|
||||
(async () => {
|
||||
/* eslint-disable arrow-body-style */
|
||||
const enrollments = await getEnrollments();
|
||||
const verifiedEnrollments = enrollments.filter((enrollment) => {
|
||||
return VERIFIED_MODES.includes(enrollment.mode);
|
||||
});
|
||||
if (verifiedEnrollments.length === 0) {
|
||||
setCanVerify(false);
|
||||
setError(ERROR_REASONS.COURSE_ENROLLMENT);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Check for an existing verification attempt
|
||||
if (existingIdVerification && !existingIdVerification.canVerify) {
|
||||
const { status } = existingIdVerification;
|
||||
setCanVerify(false);
|
||||
if (status === 'pending' || status === 'approved') {
|
||||
setError(ERROR_REASONS.EXISTING_REQUEST);
|
||||
} else {
|
||||
setError(ERROR_REASONS.CANNOT_VERIFY);
|
||||
}
|
||||
}
|
||||
}, [existingIdVerification]);
|
||||
|
||||
// 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} />
|
||||
);
|
||||
if (!canVerify) {
|
||||
return <AccessBlocked error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -95,4 +130,5 @@ export {
|
||||
IdVerificationContext,
|
||||
IdVerificationContextProvider,
|
||||
MEDIA_ACCESS,
|
||||
ERROR_REASONS,
|
||||
};
|
||||
|
||||
@@ -80,11 +80,8 @@ function IdVerificationPage(props) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
IdVerificationPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default connect(idVerificationSelector, {
|
||||
})(injectIntl(IdVerificationPage));
|
||||
|
||||
|
||||
@@ -29,6 +29,25 @@ export async function getExistingIdVerification() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the learner's enrollments. Used to check whether the learner is enrolled
|
||||
* in a verified course mode.
|
||||
*
|
||||
* Returns an array: [{...data, mode: String}]
|
||||
*/
|
||||
export async function getEnrollments() {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
|
||||
const requestConfig = {
|
||||
headers: { Accept: 'application/json' },
|
||||
};
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url, requestConfig);
|
||||
return data;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit ID verifiction to LMS.
|
||||
*
|
||||
|
||||
@@ -5,29 +5,31 @@ import { render, cleanup, act, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import ExistingRequest from '../ExistingRequest';
|
||||
|
||||
const IntlExistingRequest = injectIntl(ExistingRequest);
|
||||
import { ERROR_REASONS } from '../IdVerificationContext';
|
||||
import AccessBlocked from '../AccessBlocked';
|
||||
|
||||
const IntlAccessBlocked = injectIntl(AccessBlocked);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('ExistingRequest', () => {
|
||||
describe('AccessBlocked', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
status: '',
|
||||
error: '',
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders correctly when status is pending', async () => {
|
||||
defaultProps.status = 'pending';
|
||||
it('renders correctly when there is an existing request', async () => {
|
||||
defaultProps.error = ERROR_REASONS.EXISTING_REQUEST;
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IntlExistingRequest {...defaultProps} />
|
||||
<IntlAccessBlocked {...defaultProps} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
@@ -37,29 +39,29 @@ describe('ExistingRequest', () => {
|
||||
expect(text).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly when status is approved', async () => {
|
||||
defaultProps.status = 'approved';
|
||||
it('renders correctly when learner is not enrolled in a verified course mode', async () => {
|
||||
defaultProps.error = ERROR_REASONS.COURSE_ENROLLMENT;
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IntlExistingRequest {...defaultProps} />
|
||||
<IntlAccessBlocked {...defaultProps} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
|
||||
const text = screen.getByText(/You have already submitted your verification information./);
|
||||
const text = screen.getByText(/You are not currently enrolled in a course that requires identity verification./);
|
||||
|
||||
expect(text).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly when status is denied', async () => {
|
||||
defaultProps.status = 'denied';
|
||||
defaultProps.error = ERROR_REASONS.CANNOT_VERIFY;
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IntlExistingRequest {...defaultProps} />
|
||||
<IntlAccessBlocked {...defaultProps} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
@@ -3,11 +3,12 @@ import { render, cleanup, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { getExistingIdVerification } from '../data/service';
|
||||
import { getExistingIdVerification, getEnrollments } from '../data/service';
|
||||
import { IdVerificationContextProvider } from '../IdVerificationContext';
|
||||
|
||||
jest.mock('../data/service', () => ({
|
||||
getExistingIdVerification: jest.fn(),
|
||||
getEnrollments: jest.fn(() => []),
|
||||
}));
|
||||
|
||||
describe('IdVerificationContext', () => {
|
||||
@@ -20,7 +21,7 @@ describe('IdVerificationContext', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders correctly and calls getExistingIdVerification', async () => {
|
||||
it('renders correctly and calls getExistingIdVerification + getEnrollments', async () => {
|
||||
await act(async () => render((
|
||||
<AppContext.Provider value={{ authenticatedUser: { userId: 3 } }}>
|
||||
<IntlProvider locale="en">
|
||||
@@ -29,5 +30,6 @@ describe('IdVerificationContext', () => {
|
||||
</AppContext.Provider>
|
||||
)));
|
||||
expect(getExistingIdVerification).toHaveBeenCalled();
|
||||
expect(getEnrollments).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user