fix: Fix Flickering in IDV Panel Loading and Inconsistent Panel Loading
[MST-1099](https://openedx.atlassian.net/browse/MST-1099) Due to a bug in the way the canVerify React state is being set, when the verified name feature is enabled, the code inconsistently loads either the AccessBlocked panel or the ReviewRequirementsPanel when the learner should be allowed to verify. If the learner is ineligible to complete IDV due to their IDV history (e.g. they already have an approved IDV), and the verified name feature is enabled, the learner should be able to verify and should not see the AccessBlocked panel. This code change fixes this bug. This code change also fixes flickering that occurs when IDV is loading. This code change also includes some copy fixes.
This commit is contained in:
23
src/hooks.js
Normal file
23
src/hooks.js
Normal file
@@ -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];
|
||||
}
|
||||
@@ -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': {
|
||||
|
||||
@@ -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 <PageLoading srMessage="Loading verification status" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
46
src/tests/hooks.test.jsx
Normal file
46
src/tests/hooks.test.jsx
Normal file
@@ -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 && <div>loading</div> }
|
||||
<div>{ callData }</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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(<TestUseAsyncCallHookComponent asyncFunc={mockAsyncFunc} />);
|
||||
|
||||
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(<TestUseAsyncCallHookComponent asyncFunc={mockAsyncFunc} />);
|
||||
|
||||
await waitFor(() => (expect(mockAsyncFunc).toHaveBeenCalledTimes(1)));
|
||||
expect(queryByText('data')).toBeNull();
|
||||
});
|
||||
it('returns isLoading correctly', async () => {
|
||||
const mockAsyncFunc = jest.fn(async () => {});
|
||||
|
||||
const { queryByText } = render(<TestUseAsyncCallHookComponent asyncFunc={mockAsyncFunc} />);
|
||||
expect(queryByText('loading')).not.toBeNull();
|
||||
expect(queryByText('data')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user