From 0a4dcb4eaa7db8f4ec60eabaea81359ebe301d23 Mon Sep 17 00:00:00 2001 From: Bianca Severino Date: Mon, 7 Dec 2020 10:18:40 -0500 Subject: [PATCH] Revert "Revert "Prevent IDV access from audit courses"" --- src/id-verification/AccessBlocked.jsx | 48 +++++++++++++++++ src/id-verification/ExistingRequest.jsx | 37 ------------- .../IdVerification.messages.js | 15 ++++-- src/id-verification/IdVerificationContext.jsx | 52 ++++++++++++++++--- src/id-verification/IdVerificationPage.jsx | 3 -- src/id-verification/data/service.js | 19 +++++++ ...equest.test.jsx => AccessBlocked.test.jsx} | 28 +++++----- .../tests/IdVerificationContext.test.jsx | 6 ++- 8 files changed, 140 insertions(+), 68 deletions(-) create mode 100644 src/id-verification/AccessBlocked.jsx delete mode 100644 src/id-verification/ExistingRequest.jsx rename src/id-verification/tests/{ExistingRequest.test.jsx => AccessBlocked.test.jsx} (62%) diff --git a/src/id-verification/AccessBlocked.jsx b/src/id-verification/AccessBlocked.jsx new file mode 100644 index 0000000..eb7a9a9 --- /dev/null +++ b/src/id-verification/AccessBlocked.jsx @@ -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

{intl.formatMessage(messages['id.verification.access.blocked.enrollment'])}

; + } else if (error === ERROR_REASONS.EXISTING_REQUEST) { + return

{intl.formatMessage(messages['id.verification.access.blocked.pending'])}

; + } + return ( + no-reply@registration.edx.org, + }} + /> + ); + }; + + return ( +
+

+ {intl.formatMessage(messages['id.verification.access.blocked.title'])} +

+ {handleMessage()} +
+ + {intl.formatMessage(messages['id.verification.return.dashboard'])} + +
+
+ ); +} + +AccessBlocked.propTypes = { + intl: intlShape.isRequired, + error: PropTypes.string.isRequired, +}; + +export default injectIntl(AccessBlocked); diff --git a/src/id-verification/ExistingRequest.jsx b/src/id-verification/ExistingRequest.jsx deleted file mode 100644 index e4155c1..0000000 --- a/src/id-verification/ExistingRequest.jsx +++ /dev/null @@ -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 ( -
-

- {props.intl.formatMessage(messages['id.verification.existing.request.title'])} -

- {props.status === 'pending' || props.status === 'approved' - ?

{props.intl.formatMessage(messages['id.verification.existing.request.pending.text'])}

- : no-reply@registration.edx.org, - }} - /> - } -
- - {props.intl.formatMessage(messages['id.verification.return.dashboard'])} - -
-
- ); -} - -ExistingRequest.propTypes = { - intl: intlShape.isRequired, -}; - -export default injectIntl(ExistingRequest); diff --git a/src/id-verification/IdVerification.messages.js b/src/id-verification/IdVerification.messages.js index 8383680..52bcc56 100644 --- a/src/id-verification/IdVerification.messages.js +++ b/src/id-verification/IdVerification.messages.js @@ -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.', }, diff --git a/src/id-verification/IdVerificationContext.jsx b/src/id-verification/IdVerificationContext.jsx index 792092b..a4964df 100644 --- a/src/id-verification/IdVerificationContext.jsx +++ b/src/id-verification/IdVerificationContext.jsx @@ -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 ; } - if (!existingIdVerification.canVerify) { - const { status } = existingIdVerification; - return ( - - ); + if (!canVerify) { + return ; } return ( @@ -95,4 +130,5 @@ export { IdVerificationContext, IdVerificationContextProvider, MEDIA_ACCESS, + ERROR_REASONS, }; diff --git a/src/id-verification/IdVerificationPage.jsx b/src/id-verification/IdVerificationPage.jsx index dfdebf5..8d6e236 100644 --- a/src/id-verification/IdVerificationPage.jsx +++ b/src/id-verification/IdVerificationPage.jsx @@ -80,11 +80,8 @@ function IdVerificationPage(props) { ); } - IdVerificationPage.propTypes = { intl: intlShape.isRequired, }; - export default connect(idVerificationSelector, { })(injectIntl(IdVerificationPage)); - diff --git a/src/id-verification/data/service.js b/src/id-verification/data/service.js index f2b02c0..f3eff39 100644 --- a/src/id-verification/data/service.js +++ b/src/id-verification/data/service.js @@ -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. * diff --git a/src/id-verification/tests/ExistingRequest.test.jsx b/src/id-verification/tests/AccessBlocked.test.jsx similarity index 62% rename from src/id-verification/tests/ExistingRequest.test.jsx rename to src/id-verification/tests/AccessBlocked.test.jsx index cee4323..542491c 100644 --- a/src/id-verification/tests/ExistingRequest.test.jsx +++ b/src/id-verification/tests/AccessBlocked.test.jsx @@ -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(( - + ))); @@ -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(( - + ))); - 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(( - + ))); diff --git a/src/id-verification/tests/IdVerificationContext.test.jsx b/src/id-verification/tests/IdVerificationContext.test.jsx index 87f9b83..790f24f 100644 --- a/src/id-verification/tests/IdVerificationContext.test.jsx +++ b/src/id-verification/tests/IdVerificationContext.test.jsx @@ -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(( @@ -29,5 +30,6 @@ describe('IdVerificationContext', () => { ))); expect(getExistingIdVerification).toHaveBeenCalled(); + expect(getEnrollments).toHaveBeenCalled(); }); });