diff --git a/src/hooks.js b/src/hooks.js new file mode 100644 index 0000000..6cec54c --- /dev/null +++ b/src/hooks.js @@ -0,0 +1,23 @@ +import { useEffect, useState } from 'react'; + +// eslint-disable-next-line import/prefer-default-export +export function useAsyncCall(asyncFunc) { + const [isLoading, setIsLoading] = useState(); + const [data, setData] = useState(); + + useEffect( + () => { + (async () => { + setIsLoading(true); + const response = await asyncFunc(); + setIsLoading(false); + if (response) { + setData(response); + } + })(); + }, + [asyncFunc], + ); + + return [isLoading, data]; +} diff --git a/src/id-verification/IdVerification.messages.js b/src/id-verification/IdVerification.messages.js index 2712557..a7f0879 100644 --- a/src/id-verification/IdVerification.messages.js +++ b/src/id-verification/IdVerification.messages.js @@ -528,7 +528,7 @@ const messages = defineMessages({ }, 'id.verification.name.check.title': { id: 'id.verification.name.check.title', - defaultMessage: 'Double-check your name', + 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': { @@ -538,7 +538,7 @@ const messages = defineMessages({ }, '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.', + 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.', description: 'Text to instruct the user to check that the name displayed on the page matches what is on their government-issued ID.', }, 'id.verification.name.check.mismatch.information': { diff --git a/src/id-verification/IdVerificationContextProvider.jsx b/src/id-verification/IdVerificationContextProvider.jsx index 9dd7d2c..4acd4a8 100644 --- a/src/id-verification/IdVerificationContextProvider.jsx +++ b/src/id-verification/IdVerificationContextProvider.jsx @@ -4,6 +4,7 @@ import { AppContext } from '@edx/frontend-platform/react'; import { getProfileDataManager } from '../account-settings/data/service'; import PageLoading from '../account-settings/PageLoading'; +import { useAsyncCall } from '../hooks'; import { getExistingIdVerification, getEnrollments } from './data/service'; import AccessBlocked from './AccessBlocked'; @@ -13,16 +14,17 @@ import { VerifiedNameContext } from './VerifiedNameContext'; export default function IdVerificationContextProvider({ children }) { const { authenticatedUser } = useContext(AppContext); - const { verifiedName, verifiedNameEnabled } = useContext(VerifiedNameContext); + const { isVerifiedNameHistoryLoading, verifiedName, verifiedNameEnabled } = useContext(VerifiedNameContext); + // Call verification status endpoint to check whether we can verify. const [existingIdVerification, setExistingIdVerification] = useState(null); + const [isIDVerificationLoading, idVerificationData] = useAsyncCall(getExistingIdVerification); + const [isEnrollmentsLoading, enrollmentsData] = useAsyncCall(getEnrollments); useEffect(() => { - // Call verification status endpoint to check whether we can verify. - (async () => { - const existingIdV = await getExistingIdVerification(); - setExistingIdVerification(existingIdV); - })(); - }, []); + if (idVerificationData) { + setExistingIdVerification(idVerificationData); + } + }, [idVerificationData]); const [facePhotoFile, setFacePhotoFile] = useState(null); const [idPhotoFile, setIdPhotoFile] = useState(null); @@ -45,22 +47,22 @@ export default function IdVerificationContextProvider({ children }) { } else { setError(ERROR_REASONS.CANNOT_VERIFY); } + } else if (verifiedNameEnabled) { + setCanVerify(true); } }, [existingIdVerification, verifiedNameEnabled]); + 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 (!isEnrollmentsLoading && enrollmentsData) { + const verifiedEnrollments = enrollmentsData.filter((enrollment) => ( + VERIFIED_MODES.includes(enrollment.mode) + )); if (verifiedEnrollments.length === 0) { setCanVerify(false); setError(ERROR_REASONS.COURSE_ENROLLMENT); } - })(); - }, []); + } + }, [enrollmentsData]); const [profileDataManager, setProfileDataManager] = useState(null); useEffect(() => { @@ -140,7 +142,7 @@ export default function IdVerificationContextProvider({ children }) { }; // If we are waiting for verification status endpoint, show spinner. - if (!existingIdVerification) { + if (isIDVerificationLoading || isVerifiedNameHistoryLoading) { return ; } diff --git a/src/id-verification/VerifiedNameContext.jsx b/src/id-verification/VerifiedNameContext.jsx index d4f94c1..69f14a2 100644 --- a/src/id-verification/VerifiedNameContext.jsx +++ b/src/id-verification/VerifiedNameContext.jsx @@ -3,31 +3,29 @@ import PropTypes from 'prop-types'; import { getVerifiedNameHistory } from '../account-settings/data/service'; import { getMostRecentApprovedOrPendingVerifiedName } from '../utils'; +import { useAsyncCall } from '../hooks'; export const VerifiedNameContext = createContext(); export function VerifiedNameContextProvider({ children }) { const [verifiedNameEnabled, setVerifiedNameEnabled] = useState(false); const [verifiedName, setVerifiedName] = useState(''); - useEffect(() => { - // Make API call to retrieve VerifiedName history for the learner. - // From this information, derive whether the verified name feature is enabled - // and the learner's verified name as it should be displayed during the IDV process. - (async () => { - const response = await getVerifiedNameHistory(); - if (response) { - const { verified_name_enabled: verifiedNameFeatureEnabled, results } = response; - setVerifiedNameEnabled(verifiedNameFeatureEnabled); + const [isVerifiedNameHistoryLoading, verifiedNameHistory] = useAsyncCall(getVerifiedNameHistory); - if (verifiedNameFeatureEnabled) { - const applicableVerifiedName = getMostRecentApprovedOrPendingVerifiedName(results); - setVerifiedName(applicableVerifiedName); - } + useEffect(() => { + if (verifiedNameHistory) { + const { verified_name_enabled: verifiedNameFeatureEnabled, results } = verifiedNameHistory; + setVerifiedNameEnabled(verifiedNameFeatureEnabled); + + if (verifiedNameFeatureEnabled) { + const applicableVerifiedName = getMostRecentApprovedOrPendingVerifiedName(results); + setVerifiedName(applicableVerifiedName); } - })(); - }, []); + } + }, [verifiedNameHistory]); const value = { + isVerifiedNameHistoryLoading, verifiedNameEnabled, verifiedName, }; diff --git a/src/tests/hooks.test.jsx b/src/tests/hooks.test.jsx new file mode 100644 index 0000000..e3ce7f7 --- /dev/null +++ b/src/tests/hooks.test.jsx @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import { render, waitFor } from '@testing-library/react'; + +import { useAsyncCall } from '../hooks'; + +const TestUseAsyncCallHookComponent = (props) => { + const { asyncFunc } = props; + const [isCallLoading, callData] = useAsyncCall(asyncFunc); + + return ( + <> + { isCallLoading &&
loading
} +
{ callData }
+ + ); +}; + +TestUseAsyncCallHookComponent.propTypes = { + asyncFunc: PropTypes.func.isRequired, +}; + +describe('useAsyncCall mock', () => { + it('returns data correctly for response', async () => { + const mockAsyncFunc = jest.fn(async () => ('data')); + + const { queryByText } = render(); + + await waitFor(() => (expect(mockAsyncFunc).toHaveBeenCalledTimes(1))); + expect(queryByText('data')).not.toBeNull(); + }); + it('returns data correctly for no response', async () => { + const mockAsyncFunc = jest.fn(async () => {}); + + const { queryByText } = render(); + + await waitFor(() => (expect(mockAsyncFunc).toHaveBeenCalledTimes(1))); + expect(queryByText('data')).toBeNull(); + }); + it('returns isLoading correctly', async () => { + const mockAsyncFunc = jest.fn(async () => {}); + + const { queryByText } = render(); + expect(queryByText('loading')).not.toBeNull(); + expect(queryByText('data')).toBeNull(); + }); +});