From f71033232e7dc5125407f990139b469b0aa194e6 Mon Sep 17 00:00:00 2001 From: Bianca Severino Date: Fri, 12 Mar 2021 11:42:39 -0500 Subject: [PATCH] Prevent user from attempting name change during IDV if their account is managed by a third party --- .../IdVerification.messages.js | 5 + .../IdVerificationContextProvider.jsx | 99 +++++++++++-------- src/id-verification/panels/GetNameIdPanel.jsx | 54 +++++++--- .../panels/ReviewRequirementsPanel.jsx | 23 ++++- src/id-verification/panels/SummaryPanel.jsx | 62 ++++++++---- .../IdVerificationContextProvider.test.jsx | 32 +++++- .../tests/panels/GetNameIdPanel.test.jsx | 58 ++++++----- .../panels/ReviewRequirementsPanel.test.jsx | 45 +++++---- .../tests/panels/SummaryPanel.test.jsx | 7 ++ 9 files changed, 265 insertions(+), 120 deletions(-) diff --git a/src/id-verification/IdVerification.messages.js b/src/id-verification/IdVerification.messages.js index 51a4edf..823f781 100644 --- a/src/id-verification/IdVerification.messages.js +++ b/src/id-verification/IdVerification.messages.js @@ -6,6 +6,11 @@ const messages = defineMessages({ defaultMessage: 'Next', description: 'Next button.', }, + 'id.verification.support': { + id: 'id.verification.support', + defaultMessage: 'support', + description: 'Website support.', + }, 'id.verification.example.card.alt': { id: 'id.verification.example.card.alt', defaultMessage: 'Example of a valid identification card with a full name and photo.', diff --git a/src/id-verification/IdVerificationContextProvider.jsx b/src/id-verification/IdVerificationContextProvider.jsx index fb6ab52..e296246 100644 --- a/src/id-verification/IdVerificationContextProvider.jsx +++ b/src/id-verification/IdVerificationContextProvider.jsx @@ -2,24 +2,74 @@ import React, { useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import { AppContext } from '@edx/frontend-platform/react'; -import { hasGetUserMediaSupport } from './getUserMediaShim'; -import { getExistingIdVerification, getEnrollments } from './data/service'; +import { getProfileDataManager } from '../account-settings/data/service'; import PageLoading from '../account-settings/PageLoading'; + +import { getExistingIdVerification, getEnrollments } from './data/service'; import AccessBlocked from './AccessBlocked'; +import { hasGetUserMediaSupport } from './getUserMediaShim'; import IdVerificationContext, { MEDIA_ACCESS, ERROR_REASONS, VERIFIED_MODES } from './IdVerificationContext'; export default function IdVerificationContextProvider({ children }) { + const { authenticatedUser } = useContext(AppContext); + const [existingIdVerification, setExistingIdVerification] = useState(null); + useEffect(() => { + // Call verification status endpoint to check whether we can verify. + (async () => { + const existingIdV = await getExistingIdVerification(); + setExistingIdVerification(existingIdV); + })(); + }, []); + const [facePhotoFile, setFacePhotoFile] = useState(null); const [idPhotoFile, setIdPhotoFile] = useState(null); const [idPhotoName, setIdPhotoName] = useState(null); const [mediaStream, setMediaStream] = useState(null); - const [mediaAccess, setMediaAccess] = useState(hasGetUserMediaSupport - ? MEDIA_ACCESS.PENDING - : MEDIA_ACCESS.UNSUPPORTED); + const [mediaAccess, setMediaAccess] = useState( + hasGetUserMediaSupport ? MEDIA_ACCESS.PENDING : MEDIA_ACCESS.UNSUPPORTED, + ); + const [canVerify, setCanVerify] = useState(true); const [error, setError] = useState(''); - const { authenticatedUser } = useContext(AppContext); + 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]); + 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); + } + })(); + }, []); + + const [profileDataManager, setProfileDataManager] = useState(null); + useEffect(() => { + // Determine if the user's profile data is managed by a third-party identity provider. + // If so, they cannot update their account name manually. + if (authenticatedUser.roles.length > 0) { + setProfileDataManager( + getProfileDataManager(authenticatedUser.username, authenticatedUser.roles), + ); + } + }, [authenticatedUser]); + const [optimizelyExperimentName, setOptimizelyExperimentName] = useState(''); const contextValue = { @@ -31,6 +81,7 @@ export default function IdVerificationContextProvider({ children }) { mediaAccess, userId: authenticatedUser.userId, nameOnAccount: authenticatedUser.name, + profileDataManager, optimizelyExperimentName, setExistingIdVerification, setFacePhotoFile, @@ -58,42 +109,6 @@ export default function IdVerificationContextProvider({ children }) { }, }; - 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 ; diff --git a/src/id-verification/panels/GetNameIdPanel.jsx b/src/id-verification/panels/GetNameIdPanel.jsx index e66edd0..ef9b6e0 100644 --- a/src/id-verification/panels/GetNameIdPanel.jsx +++ b/src/id-verification/panels/GetNameIdPanel.jsx @@ -1,10 +1,11 @@ import React, { useContext, useState, useEffect, useRef, } from 'react'; -import { Form } from '@edx/paragon'; +import { getConfig } from '@edx/frontend-platform'; +import { Hyperlink, Form } from '@edx/paragon'; import { Link, useHistory } from 'react-router-dom'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; import { useNextPanelSlug } from '../routing-utilities'; import BasePanel from './BasePanel'; @@ -20,14 +21,14 @@ function GetNameIdPanel(props) { const nextPanelSlug = useNextPanelSlug(panelSlug); const { - nameOnAccount, userId, idPhotoName, setIdPhotoName, + nameOnAccount, userId, profileDataManager, idPhotoName, setIdPhotoName, } = useContext(IdVerificationContext); const nameOnAccountValue = nameOnAccount || ''; const invalidName = !nameMatches && (!idPhotoName || idPhotoName === nameOnAccount); const blankName = !nameOnAccount && !idPhotoName; useEffect(() => { - setIdPhotoName(''); + setIdPhotoName(null); }, []); useEffect(() => { @@ -45,6 +46,39 @@ function GetNameIdPanel(props) { } }, [nameMatches, blankName]); + const getNameValue = () => { + if (!nameMatches) { + // If we just check that idPhotoName exists, an empty string will cause + // the field to reset to nameOnAccountValue, so check that it is a string here. + if (typeof idPhotoName === 'string') { + return idPhotoName; + } + return nameOnAccountValue; + } + return nameOnAccountValue; + }; + + const getErrorMessage = () => { + if (profileDataManager) { + return ( + {profileDataManager}, + support: ( + + {props.intl.formatMessage(messages['id.verification.support'])} + + ), + }} + /> + ); + } + return props.intl.formatMessage(messages['id.verification.account.name.error']); + }; + const handleSubmit = (e) => { e.preventDefault(); // If the input is empty, or if no changes have been made to the @@ -80,7 +114,7 @@ function GetNameIdPanel(props) { inline onChange={() => { setNameMatches(true); - setIdPhotoName(''); + setIdPhotoName(null); }} /> setIdPhotoName(e.target.value)} data-testid="name-input" /> - {props.intl.formatMessage(messages['id.verification.account.name.error'])} + {getErrorMessage()} diff --git a/src/id-verification/panels/ReviewRequirementsPanel.jsx b/src/id-verification/panels/ReviewRequirementsPanel.jsx index a7139c1..dbcf7a1 100644 --- a/src/id-verification/panels/ReviewRequirementsPanel.jsx +++ b/src/id-verification/panels/ReviewRequirementsPanel.jsx @@ -1,7 +1,9 @@ import React, { useEffect, useContext } from 'react'; import { Link } from 'react-router-dom'; +import { getConfig } from '@edx/frontend-platform'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Alert, Hyperlink } from '@edx/paragon'; import { useNextPanelSlug } from '../routing-utilities'; import BasePanel from './BasePanel'; @@ -11,7 +13,9 @@ import messages from '../IdVerification.messages'; import exampleCard from '../assets/example-card.png'; function ReviewRequirementsPanel(props) { - const { userId, setOptimizelyExperimentName } = useContext(IdVerificationContext); + const { + userId, profileDataManager, setOptimizelyExperimentName, + } = useContext(IdVerificationContext); const panelSlug = 'review-requirements'; const nextPanelSlug = useNextPanelSlug(panelSlug); @@ -42,6 +46,23 @@ function ReviewRequirementsPanel(props) { title={props.intl.formatMessage(messages['id.verification.requirements.title'])} focusOnMount={false} > + {profileDataManager && ( + + {profileDataManager}, + support: ( + + {props.intl.formatMessage(messages['id.verification.support'])} + + ), + }} + /> + + )}

{props.intl.formatMessage(messages['id.verification.requirements.description'])}

diff --git a/src/id-verification/panels/SummaryPanel.jsx b/src/id-verification/panels/SummaryPanel.jsx index 04b27b6..7bed5c9 100644 --- a/src/id-verification/panels/SummaryPanel.jsx +++ b/src/id-verification/panels/SummaryPanel.jsx @@ -1,7 +1,7 @@ import React, { useState, useContext } from 'react'; -import { history } from '@edx/frontend-platform'; +import { getConfig, history } from '@edx/frontend-platform'; import { - Input, Button, Spinner, Alert, + Alert, Hyperlink, Input, Button, Spinner, } from '@edx/paragon'; import { Link } from 'react-router-dom'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; @@ -21,6 +21,7 @@ function SummaryPanel(props) { const { facePhotoFile, idPhotoFile, + profileDataManager, nameOnAccount, idPhotoName, stopUserMedia, @@ -111,7 +112,7 @@ function SummaryPanel(props) {

-
-