Ttracy/microba 1192 early with info display behavior (#497)
* [feat] Add ID verification Alert to course home if a user has a verified seat, but is in the unverified certificate status state, the certificateStatusAlert will now show a message letting the learner know they need to verify in order to earn a certificate. This does not remove the message about the verification deadline in the right sidebar of the course home.
This commit is contained in:
@@ -659,6 +659,40 @@ describe('Outline Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.queryByText('Your grade and certificate will be ready soon!')).toBeInTheDocument();
|
expect(screen.queryByText('Your grade and certificate will be ready soon!')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
it('renders verification alert', async () => {
|
||||||
|
const now = new Date();
|
||||||
|
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({
|
||||||
|
cert_data: {
|
||||||
|
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
||||||
|
cert_web_view_url: null,
|
||||||
|
download_url: null,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
date_blocks: [
|
||||||
|
{
|
||||||
|
date_type: 'course-end-date',
|
||||||
|
date: yesterday.toISOString(),
|
||||||
|
title: 'End',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date_type: 'certificate-available-date',
|
||||||
|
date: tomorrow.toISOString(),
|
||||||
|
title: 'Cert Available',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date_type: 'verification-deadline-date',
|
||||||
|
date: tomorrow.toISOString(),
|
||||||
|
link_text: 'Verify',
|
||||||
|
title: 'Verification Upgrade Deadline',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.queryByText('Verify your identity to earn a certificate!')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ import {
|
|||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Alert, Button } from '@edx/paragon';
|
import { Alert, Button } from '@edx/paragon';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faCheckCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import certMessages from './messages';
|
import certMessages from './messages';
|
||||||
import certStatusMessages from '../../../progress-tab/certificate-status/messages';
|
import certStatusMessages from '../../../progress-tab/certificate-status/messages';
|
||||||
|
|
||||||
export const CERT_STATUS_TYPE = {
|
export const CERT_STATUS_TYPE = {
|
||||||
EARNED_NOT_AVAILABLE: 'earned_but_not_available',
|
EARNED_NOT_AVAILABLE: 'earned_but_not_available',
|
||||||
DOWNLOADABLE: 'downloadable',
|
DOWNLOADABLE: 'downloadable',
|
||||||
|
UNVERIFIED: 'unverified',
|
||||||
};
|
};
|
||||||
|
|
||||||
function CertificateStatusAlert({ intl, payload }) {
|
function CertificateStatusAlert({ intl, payload }) {
|
||||||
@@ -27,65 +29,109 @@ function CertificateStatusAlert({ intl, payload }) {
|
|||||||
userTimezone,
|
userTimezone,
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
let variant = '';
|
// eslint-disable-next-line react/prop-types
|
||||||
if (certStatusType === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE || certStatusType === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
const AlertWrapper = (props) => props.children(props);
|
||||||
variant = 'success';
|
|
||||||
}
|
|
||||||
|
|
||||||
let header = '';
|
const renderCertAwardedStatus = () => {
|
||||||
let body = '';
|
const alertProps = {
|
||||||
let buttonVisible = false;
|
variant: 'success',
|
||||||
let buttonMessage = '';
|
icon: faCheckCircle,
|
||||||
|
iconClassName: 'alert-icon text-success-500',
|
||||||
if (certStatusType === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
|
};
|
||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
if (certStatusType === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
|
||||||
const certificateAvailableDateFormatted = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />;
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
const courseEndDateFormatted = <FormattedDate value={courseEndDate} day="numeric" month="long" year="numeric" />;
|
const certificateAvailableDateFormatted = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />;
|
||||||
|
const courseEndDateFormatted = <FormattedDate value={courseEndDate} day="numeric" month="long" year="numeric" />;
|
||||||
header = intl.formatMessage(certMessages.certStatusEarnedNotAvailableHeader);
|
alertProps.header = intl.formatMessage(certMessages.certStatusEarnedNotAvailableHeader);
|
||||||
body = (
|
alertProps.body = (
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="learning.outline.alert.cert.when"
|
id="learning.outline.alert.cert.when"
|
||||||
defaultMessage="This course ended on {courseEndDateFormatted}. Final grades and certificates are
|
defaultMessage="This course ended on {courseEndDateFormatted}. Final grades and certificates are
|
||||||
scheduled to be available after {certificateAvailableDate}."
|
scheduled to be available after {certificateAvailableDate}."
|
||||||
values={{
|
values={{
|
||||||
courseEndDateFormatted,
|
courseEndDateFormatted,
|
||||||
certificateAvailableDate: certificateAvailableDateFormatted,
|
certificateAvailableDate: certificateAvailableDateFormatted,
|
||||||
}}
|
}}
|
||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
} else if (certStatusType === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
} else if (certStatusType === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
||||||
header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
|
alertProps.header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
|
||||||
if (isWebCert) {
|
if (isWebCert) {
|
||||||
buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
|
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
|
||||||
} else {
|
} else {
|
||||||
buttonMessage = intl.formatMessage(certStatusMessages.downloadableButton);
|
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.downloadableButton);
|
||||||
|
}
|
||||||
|
alertProps.buttonVisible = true;
|
||||||
|
alertProps.buttonLink = certURL;
|
||||||
}
|
}
|
||||||
buttonVisible = true;
|
return alertProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNotIDVerifiedStatus = () => {
|
||||||
|
const alertProps = {
|
||||||
|
variant: 'warning',
|
||||||
|
icon: faExclamationTriangle,
|
||||||
|
iconClassName: 'alert-icon text-warning-500',
|
||||||
|
header: intl.formatMessage(certStatusMessages.unverifiedHomeHeader),
|
||||||
|
buttonMessage: intl.formatMessage(certStatusMessages.unverifiedHomeButton),
|
||||||
|
body: intl.formatMessage(certStatusMessages.unverifiedHomeBody),
|
||||||
|
buttonVisible: true,
|
||||||
|
buttonLink: getConfig().SUPPORT_URL_ID_VERIFICATION,
|
||||||
|
};
|
||||||
|
|
||||||
|
return alertProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
let alertProps = {};
|
||||||
|
switch (certStatusType) {
|
||||||
|
case CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE:
|
||||||
|
case CERT_STATUS_TYPE.DOWNLOADABLE:
|
||||||
|
alertProps = renderCertAwardedStatus();
|
||||||
|
break;
|
||||||
|
case CERT_STATUS_TYPE.UNVERIFIED:
|
||||||
|
alertProps = renderNotIDVerifiedStatus();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert variant={variant}>
|
<AlertWrapper {...alertProps}>
|
||||||
<div className="row justify-content-between align-items-center">
|
{({
|
||||||
<div className={buttonVisible ? '' : 'col-auto'}>
|
variant,
|
||||||
<FontAwesomeIcon icon={faCheckCircle} className="alert-icon text-success-500" />
|
buttonVisible,
|
||||||
<Alert.Heading>{header}</Alert.Heading>
|
iconClassName,
|
||||||
{body}
|
icon,
|
||||||
</div>
|
header,
|
||||||
{buttonVisible && (
|
buttonLink,
|
||||||
<div className="m-auto m-lg-0 pr-lg-3">
|
body,
|
||||||
<Button
|
buttonMessage,
|
||||||
variant="primary"
|
}) => (
|
||||||
href={certURL}
|
<Alert variant={variant}>
|
||||||
>
|
<div className="row justify-content-between align-items-center">
|
||||||
{buttonMessage}
|
<div className={buttonVisible ? 'col-lg-8' : 'col-auto'}>
|
||||||
</Button>
|
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||||
|
<Alert.Heading>{header}</Alert.Heading>
|
||||||
|
{body}
|
||||||
|
</div>
|
||||||
|
{buttonVisible && (
|
||||||
|
<div className="m-auto m-lg-0 pr-lg-3">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
href={buttonLink}
|
||||||
|
>
|
||||||
|
{buttonMessage}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Alert>
|
||||||
</div>
|
|
||||||
</Alert>
|
)}
|
||||||
|
</AlertWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ function verifyCertStatusType(status) {
|
|||||||
if (status === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
|
if (status === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
|
||||||
return CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE;
|
return CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE;
|
||||||
}
|
}
|
||||||
|
if (status === CERT_STATUS_TYPE.UNVERIFIED) {
|
||||||
|
return CERT_STATUS_TYPE.UNVERIFIED;
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +40,7 @@ function useCertificateStatusAlert(courseId) {
|
|||||||
certificateAvailableDate,
|
certificateAvailableDate,
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
} = certData || {};
|
} = certData || {};
|
||||||
|
|
||||||
const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
|
const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
|
||||||
|
|
||||||
const certStatusType = verifyCertStatusType(certStatus);
|
const certStatusType = verifyCertStatusType(certStatus);
|
||||||
const isWebCert = downloadUrl === null;
|
const isWebCert = downloadUrl === null;
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ const messages = defineMessages({
|
|||||||
id: 'progress.certificateStatus.upgradeButton',
|
id: 'progress.certificateStatus.upgradeButton',
|
||||||
defaultMessage: 'Upgrade now',
|
defaultMessage: 'Upgrade now',
|
||||||
},
|
},
|
||||||
|
unverifiedHomeHeader: {
|
||||||
|
id: 'progress.certificateStatus.unverifiedHomeHeader',
|
||||||
|
defaultMessage: 'Verify your identity to earn a certificate!',
|
||||||
|
},
|
||||||
|
unverifiedHomeButton: {
|
||||||
|
id: 'progress.certificateStatus.unverifiedHomeButton',
|
||||||
|
defaultMessage: 'Verify my ID',
|
||||||
|
},
|
||||||
|
unverifiedHomeBody: {
|
||||||
|
id: 'progress.certificateStatus.unverifiedHomeBody',
|
||||||
|
defaultMessage: 'In order to generate a certificate for this course, you must complete the ID verification process.',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function initializeMockApp() {
|
|||||||
roles: [],
|
roles: [],
|
||||||
administrator: false,
|
administrator: false,
|
||||||
},
|
},
|
||||||
SUPPORT_URL_ID_VERIFICATION: true,
|
SUPPORT_URL_ID_VERIFICATION: 'http://example.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
const loggingService = configureLogging(MockLoggingService, {
|
const loggingService = configureLogging(MockLoggingService, {
|
||||||
|
|||||||
Reference in New Issue
Block a user