diff --git a/src/course-home/data/apiHooks.test.tsx b/src/course-home/data/apiHooks.test.tsx
index 1ae3aa186b..1278d74fb9 100644
--- a/src/course-home/data/apiHooks.test.tsx
+++ b/src/course-home/data/apiHooks.test.tsx
@@ -6,7 +6,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeMockApp } from '../../setupTest';
import { ToastProvider, useToast } from '../../generic/ToastContext';
-import { useResetDeadlines, usePostEvent } from './apiHooks';
+import { useResetDeadlines, usePostEvent, useRequestCert } from './apiHooks';
const { loggingService } = initializeMockApp();
@@ -116,4 +116,30 @@ describe('course-home apiHooks', () => {
await waitFor(() => expect(loggingService.logError).toHaveBeenCalled());
});
});
+
+ describe('useRequestCert', () => {
+ const certUrl = `${getConfig().LMS_BASE_URL}/courses/course-1/generate_user_cert`;
+
+ it('POSTs to the request-cert url', async () => {
+ axiosMock.onPost(certUrl).reply(200);
+ const { wrapper } = buildWrapper();
+ const { result } = renderHook(() => useRequestCert(), { wrapper });
+
+ await act(async () => { await result.current.mutateAsync({ courseId: 'course-1' }); });
+
+ expect(axiosMock.history.post[0].url).toEqual(certUrl);
+ });
+
+ it('logs the error when the POST fails', async () => {
+ axiosMock.onPost(certUrl).reply(500);
+ const { wrapper } = buildWrapper();
+ const { result } = renderHook(() => useRequestCert(), { wrapper });
+
+ await act(async () => {
+ await result.current.mutateAsync({ courseId: 'course-1' }).catch(() => {});
+ });
+
+ await waitFor(() => expect(loggingService.logError).toHaveBeenCalled());
+ });
+ });
});
diff --git a/src/course-home/data/apiHooks.ts b/src/course-home/data/apiHooks.ts
index 79bf434890..f5d4004ecd 100644
--- a/src/course-home/data/apiHooks.ts
+++ b/src/course-home/data/apiHooks.ts
@@ -3,7 +3,7 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { useToast, ToastContent } from '@src/generic/ToastContext';
import {
- executePostFromPostEvent, getCourseHomeCourseMetadata, getDatesTabData, postCourseDeadlines,
+ executePostFromPostEvent, getCourseHomeCourseMetadata, getDatesTabData, postCourseDeadlines, postRequestCert,
} from './api';
import { courseHomeQueryKeys } from './queryKeys';
@@ -60,3 +60,8 @@ export const useDatesTabData = (courseId: string) => useQuery({
queryFn: () => getDatesTabData(courseId),
meta: { modelType: 'dates', courseId },
});
+
+export const useRequestCert = () => useMutation({
+ mutationFn: ({ courseId }: { courseId: string }) => postRequestCert(courseId),
+ onError: (error) => logError(error),
+});
diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js
index 48acab1e02..300d2bac51 100644
--- a/src/course-home/data/thunks.js
+++ b/src/course-home/data/thunks.js
@@ -7,7 +7,6 @@ import {
deprecatedPostCourseGoals,
postWeeklyLearningGoal,
postDismissWelcomeMessage,
- postRequestCert,
getLiveTabIframe,
} from './api';
@@ -105,10 +104,6 @@ export function dismissWelcomeMessage(courseId) {
return async () => postDismissWelcomeMessage(courseId);
}
-export function requestCert(courseId) {
- return async () => postRequestCert(courseId);
-}
-
export async function deprecatedSaveCourseGoal(courseId, goalKey) {
return deprecatedPostCourseGoals(courseId, goalKey);
}
diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx
index dd7114be7e..7e542ae105 100644
--- a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx
+++ b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx
@@ -6,7 +6,6 @@ import {
useIntl,
} from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@openedx/paragon';
-import { useDispatch } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheckCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
@@ -15,7 +14,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import certMessages from './messages';
import certStatusMessages from '../../../progress-tab/certificate-status/messages';
-import { requestCert } from '../../../data/thunks';
+import { useRequestCert } from '../../../data/apiHooks';
export const CERT_STATUS_TYPE = {
EARNED_NOT_AVAILABLE: 'earned_but_not_available',
@@ -26,7 +25,7 @@ export const CERT_STATUS_TYPE = {
const CertificateStatusAlert = ({ payload }) => {
const intl = useIntl();
- const dispatch = useDispatch();
+ const requestCert = useRequestCert();
const {
certificateAvailableDate,
certStatus,
@@ -91,7 +90,7 @@ const CertificateStatusAlert = ({ payload }) => {
alertProps.buttonLink = '';
alertProps.buttonAction = () => {
sendAlertClickTracking('edx.ui.lms.course_outline.certificate_alert_request_cert_button.clicked');
- dispatch(requestCert(courseId));
+ requestCert.mutate({ courseId });
};
}
return alertProps;
diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx
index 7c1b2ce343..2abfc286f1 100644
--- a/src/course-home/progress-tab/ProgressTab.test.jsx
+++ b/src/course-home/progress-tab/ProgressTab.test.jsx
@@ -7,7 +7,7 @@ import { breakpoints } from '@openedx/paragon';
import MockAdapter from 'axios-mock-adapter';
import {
- fireEvent, initializeMockApp, logUnhandledRequests, render, screen, act,
+ fireEvent, initializeMockApp, logUnhandledRequests, render, screen, act, waitFor,
} from '../../setupTest';
import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils';
import * as thunks from '../data/thunks';
@@ -1020,6 +1020,10 @@ describe('Progress Tab', () => {
is_staff: false,
certificate_status_variant: 'requesting',
});
+
+ await waitFor(() => expect(
+ axiosMock.history.post.some(req => req.url.includes('generate_user_cert')),
+ ).toBe(true));
});
it('Displays verify identity link', async () => {
diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
index bc1cd92039..e7dd2c21f9 100644
--- a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
+++ b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
@@ -1,5 +1,4 @@
import { useEffect } from 'react';
-import { useDispatch } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
@@ -10,7 +9,7 @@ import { useContextId } from '../../../data/hooks';
import { useModel } from '../../../generic/model-store';
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
-import { requestCert } from '../../data/thunks';
+import { useRequestCert } from '../../data/apiHooks';
import messages from './messages';
import ProgressCertificateStatusSlot from '../../../plugin-slots/ProgressCertificateStatusSlot';
@@ -62,7 +61,7 @@ const CertificateStatus = () => {
courserun_key: courseId,
};
- const dispatch = useDispatch();
+ const requestCert = useRequestCert();
const { administrator } = getAuthenticatedUser();
let certStatus;
@@ -110,7 +109,7 @@ const CertificateStatus = () => {
switch (certStatus) {
case 'requesting':
certCase = 'requestable';
- buttonAction = () => { dispatch(requestCert(courseId)); };
+ buttonAction = () => { requestCert.mutate({ courseId }); };
body = intl.formatMessage(messages[`${certCase}Body`]);
buttonText = intl.formatMessage(messages[`${certCase}Button`]);
break;
diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx
index e7c6449595..11f93cb9b1 100644
--- a/src/courseware/course/course-exit/CourseCelebration.jsx
+++ b/src/courseware/course/course-exit/CourseCelebration.jsx
@@ -4,7 +4,7 @@ import { faLinkedinIn } from '@fortawesome/free-brands-svg-icons';
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import {
Alert,
breakpoints,
@@ -23,7 +23,7 @@ import certificateLocked from '../../../generic/assets/openedx_locked_certificat
import { FormattedPricing } from '../../../generic/upgrade-button';
import messages from './messages';
import { useModel } from '../../../generic/model-store';
-import { requestCert } from '../../../course-home/data/thunks';
+import { useRequestCert } from '../../../course-home/data/apiHooks';
import ProgramCompletion from './ProgramCompletion';
import UpgradeFootnote from './UpgradeFootnote';
import SocialIcons from '../../social-share/SocialIcons';
@@ -38,7 +38,7 @@ const CourseCelebration = () => {
const intl = useIntl();
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
const { courseId } = useSelector(state => state.courseware);
- const dispatch = useDispatch();
+ const requestCert = useRequestCert();
const {
certificateData,
end,
@@ -153,7 +153,7 @@ const CourseCelebration = () => {
variant={buttonVariant}
onClick={() => {
logClick(org, courseId, administrator, buttonEvent);
- dispatch(requestCert(courseId));
+ requestCert.mutate({ courseId });
}}
>
{intl.formatMessage(messages.requestCertificateButton)}
diff --git a/src/courseware/course/course-exit/CourseExit.test.jsx b/src/courseware/course/course-exit/CourseExit.test.jsx
index d0a212da95..f8655fcf9d 100644
--- a/src/courseware/course/course-exit/CourseExit.test.jsx
+++ b/src/courseware/course/course-exit/CourseExit.test.jsx
@@ -4,6 +4,7 @@ import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { fetchCourse } from '../../data';
import { buildSimpleCourseBlocks } from '../../../shared/data/__factories__/courseBlocks.factory';
@@ -139,6 +140,15 @@ describe('Course Exit Pages', () => {
expect(screen.getByRole('button', { name: 'Request certificate' })).toBeInTheDocument();
});
+ it('requests the certificate when the request certificate link is clicked', async () => {
+ setMetadata({ certificate_data: { cert_status: 'requesting' } });
+ await fetchAndRender();
+ await userEvent.click(screen.getByRole('button', { name: 'Request certificate' }));
+ await waitFor(() => expect(
+ axiosMock.history.post.some(req => req.url.includes('generate_user_cert')),
+ ).toBe(true));
+ });
+
it('Displays social share icons', async () => {
setMetadata({ certificate_data: { cert_status: 'unverified' }, marketing_url: 'https://edx.org' });
await fetchAndRender();