diff --git a/src/course-home/data/__factories__/outlineTabData.factory.js b/src/course-home/data/__factories__/outlineTabData.factory.js
index 0126a4e6..21a9ecd0 100644
--- a/src/course-home/data/__factories__/outlineTabData.factory.js
+++ b/src/course-home/data/__factories__/outlineTabData.factory.js
@@ -31,6 +31,12 @@ Factory.define('outlineTabData')
.attrs({
access_expiration: null,
can_show_upgrade_sock: false,
+ cert_data: {
+ cert_status: null,
+ cert_web_view_url: null,
+ certificate_available_date: null,
+ download_url: null,
+ },
course_goals: {
goal_options: [],
selected_goal: null,
diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap
index 208b3091..c80fb93a 100644
--- a/src/course-home/data/__snapshots__/redux.test.js.snap
+++ b/src/course-home/data/__snapshots__/redux.test.js.snap
@@ -365,6 +365,12 @@ Object {
"course-v1:edX+DemoX+Demo_Course_1": Object {
"accessExpiration": null,
"canShowUpgradeSock": false,
+ "certData": Object {
+ "certStatus": null,
+ "certWebViewUrl": null,
+ "certificateAvailableDate": null,
+ "downloadUrl": null,
+ },
"courseBlocks": Object {
"courses": Object {
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js
index 245343ee..23216374 100644
--- a/src/course-home/data/api.js
+++ b/src/course-home/data/api.js
@@ -204,6 +204,7 @@ export async function getOutlineTabData(courseId) {
const accessExpiration = camelCaseObject(data.access_expiration);
const canShowUpgradeSock = data.can_show_upgrade_sock;
+ const certData = camelCaseObject(data.cert_data);
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
const courseGoals = camelCaseObject(data.course_goals);
const courseTools = camelCaseObject(data.course_tools);
@@ -221,6 +222,7 @@ export async function getOutlineTabData(courseId) {
return {
accessExpiration,
canShowUpgradeSock,
+ certData,
courseBlocks,
courseGoals,
courseTools,
diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx
index 15da2938..d2a86cf0 100644
--- a/src/course-home/outline-tab/OutlineTab.jsx
+++ b/src/course-home/outline-tab/OutlineTab.jsx
@@ -19,7 +19,7 @@ import Section from './Section';
import UpdateGoalSelector from './widgets/UpdateGoalSelector';
import UpgradeCard from './widgets/UpgradeCard';
import useAccessExpirationAlert from '../../alerts/access-expiration-alert';
-import useCertificateAvailableAlert from './alerts/certificate-available-alert';
+import useCertificateAvailableAlert from './alerts/certificate-status-alert';
import useCourseEndAlert from './alerts/course-end-alert';
import useCourseStartAlert from './alerts/course-start-alert';
import usePrivateCourseAlert from './alerts/private-course-alert';
diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx
index 89a2b43b..f9485421 100644
--- a/src/course-home/outline-tab/OutlineTab.test.jsx
+++ b/src/course-home/outline-tab/OutlineTab.test.jsx
@@ -14,6 +14,7 @@ import {
import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils';
import * as thunks from '../data/thunks';
import initializeStore from '../../store';
+import { CERT_STATUS_TYPE } from './alerts/certificate-status-alert/CertificateStatusAlert';
import OutlineTab from './OutlineTab';
initializeMockApp();
@@ -672,7 +673,14 @@ describe('Outline Tab', () => {
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
setMetadata({ is_enrolled: true });
- setTabData({}, {
+ setTabData({
+ cert_data: {
+ cert_status: CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE,
+ cert_web_view_url: null,
+ certificate_available_date: tomorrow.toISOString(),
+ download_url: null,
+ },
+ }, {
date_blocks: [
{
date_type: 'course-end-date',
@@ -687,11 +695,63 @@ describe('Outline Tab', () => {
],
});
await fetchAndRender();
- await screen.findByText('Your grade and certificate will be ready soon!');
+ expect(screen.queryByText('Your grade and certificate will be ready soon!')).toBeInTheDocument();
});
});
});
+ describe('Certificate (web) Complete Alert', () => {
+ it('appears', async () => {
+ const now = new Date();
+ const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
+ setMetadata({ is_enrolled: true });
+ setTabData({
+ cert_data: {
+ cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
+ cert_web_view_url: 'certificate/testuuid',
+ certificate_available_date: null,
+ download_url: null,
+ },
+ }, {
+ date_blocks: [
+ {
+ date_type: 'course-end-date',
+ date: yesterday.toISOString(),
+ title: 'End',
+ },
+ ],
+ });
+ await fetchAndRender();
+ expect(screen.queryByText('Congratulations! Your certificate is ready.')).toBeInTheDocument();
+ });
+ });
+
+ describe('Certificate (pdf) Complete Alert', () => {
+ it('appears', async () => {
+ const now = new Date();
+ const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
+ setMetadata({ is_enrolled: true });
+ setTabData({
+ cert_data: {
+ cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
+ cert_web_view_url: null,
+ certificate_available_date: null,
+ download_url: 'download/url',
+ },
+ }, {
+ date_blocks: [
+ {
+ date_type: 'course-end-date',
+ date: yesterday.toISOString(),
+ title: 'End',
+ },
+ ],
+ });
+ await fetchAndRender();
+ expect(screen.queryByText('Congratulations! Your certificate is ready.')).not.toBeInTheDocument();
+ });
+ });
+
describe('Proctoring Info Panel', () => {
const onboardingReleaseDate = new Date();
onboardingReleaseDate.setDate(new Date().getDate() - 7);
diff --git a/src/course-home/outline-tab/alerts/certificate-available-alert/CertificateAvailableAlert.jsx b/src/course-home/outline-tab/alerts/certificate-available-alert/CertificateAvailableAlert.jsx
deleted file mode 100644
index 9cbe405c..00000000
--- a/src/course-home/outline-tab/alerts/certificate-available-alert/CertificateAvailableAlert.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { FormattedDate, FormattedMessage } from '@edx/frontend-platform/i18n';
-
-import { Alert, ALERT_TYPES } from '../../../../generic/user-messages';
-
-function CertificateAvailableAlert({ payload }) {
- const {
- certDate,
- userTimezone,
- courseEndDate,
- } = payload;
-
- const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
- const certificateAvailableDateFormatted = ;
- const courseEndDateFormatted = ;
-
- return (
-
-
-
-
-
-
-
- );
-}
-
-CertificateAvailableAlert.propTypes = {
- payload: PropTypes.shape({
- certDate: PropTypes.string,
- courseEndDate: PropTypes.string,
- userTimezone: PropTypes.string,
- }).isRequired,
-};
-
-export default CertificateAvailableAlert;
diff --git a/src/course-home/outline-tab/alerts/certificate-available-alert/hooks.js b/src/course-home/outline-tab/alerts/certificate-available-alert/hooks.js
deleted file mode 100644
index a176572f..00000000
--- a/src/course-home/outline-tab/alerts/certificate-available-alert/hooks.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import React, { useMemo } from 'react';
-import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
-
-import { useAlert } from '../../../../generic/user-messages';
-import { useModel } from '../../../../generic/model-store';
-
-const CertificateAvailableAlert = React.lazy(() => import('./CertificateAvailableAlert'));
-
-function useCertificateAvailableAlert(courseId) {
- const {
- isEnrolled,
- } = useModel('courseHomeMeta', courseId);
- const {
- datesWidget: {
- courseDateBlocks,
- userTimezone,
- },
- } = useModel('outline', courseId);
- const authenticatedUser = getAuthenticatedUser();
- const username = authenticatedUser ? authenticatedUser.username : '';
- const certBlock = courseDateBlocks.find(b => b.dateType === 'certificate-available-date');
- const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
- const endDate = endBlock ? new Date(endBlock.date) : null;
- const hasEnded = endBlock ? endDate < new Date() : false;
- const isVisible = isEnrolled && certBlock && hasEnded; // only show if we're between end and cert dates
- const payload = {
- certDate: certBlock && certBlock.date,
- courseEndDate: endBlock && endBlock.date,
- username,
- userTimezone,
- };
-
- useAlert(isVisible, {
- code: 'clientCertificateAvailableAlert',
- payload: useMemo(() => payload, Object.values(payload).sort()),
- topic: 'outline-course-alerts',
- });
-
- return {
- clientCertificateAvailableAlert: CertificateAvailableAlert,
- };
-}
-
-export default useCertificateAvailableAlert;
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
new file mode 100644
index 00000000..8063eaa8
--- /dev/null
+++ b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ FormattedDate,
+ FormattedMessage,
+ injectIntl,
+ intlShape,
+} from '@edx/frontend-platform/i18n';
+import { Alert, Button } from '@edx/paragon';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
+import certMessages from './messages';
+import certStatusMessages from '../../../progress-tab/certificate-status/messages';
+
+export const CERT_STATUS_TYPE = {
+ EARNED_NOT_AVAILABLE: 'earned_but_not_available',
+ DOWNLOADABLE: 'downloadable',
+};
+
+function CertificateStatusAlert({ intl, payload }) {
+ const {
+ certificateAvailableDate,
+ certStatusType,
+ courseEndDate,
+ certURL,
+ userTimezone,
+ } = payload;
+
+ let variant = '';
+ if (certStatusType === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE || certStatusType === CERT_STATUS_TYPE.DOWNLOADABLE) {
+ variant = 'success';
+ }
+
+ let header = '';
+ let body = '';
+ let buttonVisible = false;
+ let buttonMessage = '';
+ if (certStatusType === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
+ const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
+ const certificateAvailableDateFormatted = ;
+ const courseEndDateFormatted = ;
+
+ header = intl.formatMessage(certMessages.certStatusEarnedNotAvailableHeader);
+ body = (
+
+
+
+ );
+ } else if (certStatusType === CERT_STATUS_TYPE.DOWNLOADABLE) {
+ header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
+ buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
+ buttonVisible = true;
+ }
+ return (
+
+
+
+ {buttonVisible && (
+
+
+
+ )}
+
+
+ );
+}
+
+CertificateStatusAlert.propTypes = {
+ intl: intlShape.isRequired,
+ payload: PropTypes.shape({
+ certificateAvailableDate: PropTypes.string,
+ certStatusType: PropTypes.string,
+ courseEndDate: PropTypes.string,
+ certURL: PropTypes.string,
+ userTimezone: PropTypes.string,
+ }).isRequired,
+};
+
+export default injectIntl(CertificateStatusAlert);
diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js b/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js
new file mode 100644
index 00000000..7764fc70
--- /dev/null
+++ b/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js
@@ -0,0 +1,71 @@
+import React, { useMemo } from 'react';
+
+import { getConfig } from '@edx/frontend-platform';
+import { useAlert } from '../../../../generic/user-messages';
+import { useModel } from '../../../../generic/model-store';
+import { CERT_STATUS_TYPE } from './CertificateStatusAlert';
+
+const CertificateStatusAlert = React.lazy(() => import('./CertificateStatusAlert'));
+
+function verifyCertStatusType(status) {
+ // This method will only return cert statuses when we want to alert on them.
+ // It should be modified when we want to alert on a new status type.
+ if (status === CERT_STATUS_TYPE.DOWNLOADABLE) {
+ return CERT_STATUS_TYPE.DOWNLOADABLE;
+ }
+ if (status === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
+ return CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE;
+ }
+ return '';
+}
+
+function useCertificateStatusAlert(courseId) {
+ const {
+ isEnrolled,
+ } = useModel('courseHomeMeta', courseId);
+ const {
+ datesWidget: {
+ courseDateBlocks,
+ userTimezone,
+ },
+ certData,
+ } = useModel('outline', courseId);
+
+ const {
+ certStatus,
+ certWebViewUrl,
+ certificateAvailableDate,
+ downloadUrl,
+ } = certData || {};
+
+ const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
+
+ const certStatusType = verifyCertStatusType(certStatus);
+ const certURL = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
+ const hasCertStatus = certStatusType !== '';
+ const isWebCert = downloadUrl === null;
+
+ // only show if there is a known cert status that we want to alert on.
+ // TODO Temporarily only show this for WebCertificates while we update the messaging
+ // in follow on work MICROBA-678
+ const isVisible = isEnrolled && hasCertStatus && isWebCert;
+ const payload = {
+ certificateAvailableDate,
+ certURL,
+ certStatusType,
+ courseEndDate: endBlock && endBlock.date,
+ userTimezone,
+ };
+
+ useAlert(isVisible, {
+ code: 'clientCertificateStatusAlert',
+ payload: useMemo(() => payload, Object.values(payload).sort()),
+ topic: 'outline-course-alerts',
+ });
+
+ return {
+ clientCertificateStatusAlert: CertificateStatusAlert,
+ };
+}
+
+export default useCertificateStatusAlert;
diff --git a/src/course-home/outline-tab/alerts/certificate-available-alert/index.js b/src/course-home/outline-tab/alerts/certificate-status-alert/index.js
similarity index 100%
rename from src/course-home/outline-tab/alerts/certificate-available-alert/index.js
rename to src/course-home/outline-tab/alerts/certificate-status-alert/index.js
diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/messages.js b/src/course-home/outline-tab/alerts/certificate-status-alert/messages.js
new file mode 100644
index 00000000..8dfce5f6
--- /dev/null
+++ b/src/course-home/outline-tab/alerts/certificate-status-alert/messages.js
@@ -0,0 +1,16 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ certStatusEarnedNotAvailableHeader: {
+ id: 'cert.alert.earned.unavailable.header',
+ defaultMessage: 'Your grade and certificate will be ready soon!',
+ description: 'Header alerting the user that their certificate will be available soon.',
+ },
+ certStatusDownloadableHeader: {
+ id: 'cert.alert.earned.ready.header',
+ defaultMessage: 'Congratulations! Your certificate is ready.',
+ description: 'Header alerting the user that their certificate is ready.',
+ },
+});
+
+export default messages;