diff --git a/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx b/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx
index 35aecec4..f5d0307a 100644
--- a/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx
+++ b/src/alerts/access-expiration-alert/AccessExpirationAlert.jsx
@@ -1,24 +1,140 @@
import React from 'react';
import PropTypes from 'prop-types';
+import {
+ FormattedMessage, FormattedDate, injectIntl, intlShape,
+} from '@edx/frontend-platform/i18n';
+import { Hyperlink } from '@edx/paragon';
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
+import messages from './messages';
-function AccessExpirationAlert({ payload }) {
+function AccessExpirationAlert({ intl, payload }) {
const {
- rawHtml,
+ accessExpiration,
+ userTimezone,
} = payload;
- return rawHtml && (
+ const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
+
+ if (!accessExpiration) {
+ return null;
+ }
+
+ const {
+ expirationDate,
+ masqueradingExpiredCourse,
+ upgradeDeadline,
+ upgradeUrl,
+ } = accessExpiration;
+
+ if (masqueradingExpiredCourse) {
+ return (
+
+
+ ),
+ }}
+ />
+
+ );
+ }
+
+ let deadlineMessage = null;
+ if (upgradeDeadline && upgradeUrl) {
+ deadlineMessage = (
+ <>
+
+
+ ),
+ }}
+ />
+
+
+ {intl.formatMessage(messages.upgradeNow)}
+
+ >
+ );
+ }
+
+ return (
- {/* eslint-disable-next-line react/no-danger */}
-
+
+
+ ),
+ }}
+ />
+
+
+
+ ),
+ }}
+ />
+ {deadlineMessage}
);
}
AccessExpirationAlert.propTypes = {
+ intl: intlShape.isRequired,
payload: PropTypes.shape({
- rawHtml: PropTypes.string.isRequired,
+ accessExpiration: PropTypes.shape({
+ expirationDate: PropTypes.string.isRequired,
+ masqueradingExpiredCourse: PropTypes.bool.isRequired,
+ upgradeDeadline: PropTypes.string,
+ upgradeUrl: PropTypes.string,
+ }).isRequired,
+ userTimezone: PropTypes.string.isRequired,
}).isRequired,
};
-export default AccessExpirationAlert;
+export default injectIntl(AccessExpirationAlert);
diff --git a/src/alerts/access-expiration-alert/hooks.js b/src/alerts/access-expiration-alert/hooks.js
index 0aaa12a4..757ed00c 100644
--- a/src/alerts/access-expiration-alert/hooks.js
+++ b/src/alerts/access-expiration-alert/hooks.js
@@ -3,13 +3,12 @@ import { useAlert } from '../../generic/user-messages';
const AccessExpirationAlert = React.lazy(() => import('./AccessExpirationAlert'));
-function useAccessExpirationAlert(courseExpiredMessage, topic) {
- const rawHtml = courseExpiredMessage || null;
- const isVisible = !!rawHtml; // If it exists, show it.
+function useAccessExpirationAlert(accessExpiration, userTimezone, topic) {
+ const isVisible = !!accessExpiration; // If it exists, show it.
useAlert(isVisible, {
code: 'clientAccessExpirationAlert',
- payload: useMemo(() => ({ rawHtml }), [rawHtml]),
+ payload: useMemo(() => ({ accessExpiration, userTimezone }), [accessExpiration, userTimezone]),
topic,
});
diff --git a/src/alerts/access-expiration-alert/messages.js b/src/alerts/access-expiration-alert/messages.js
new file mode 100644
index 00000000..32d3f9aa
--- /dev/null
+++ b/src/alerts/access-expiration-alert/messages.js
@@ -0,0 +1,10 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ upgradeNow: {
+ id: 'learning.accessExpiration.upgradeNow',
+ defaultMessage: 'Upgrade now',
+ },
+});
+
+export default messages;
diff --git a/src/alerts/offer-alert/OfferAlert.jsx b/src/alerts/offer-alert/OfferAlert.jsx
index 16857957..376ae293 100644
--- a/src/alerts/offer-alert/OfferAlert.jsx
+++ b/src/alerts/offer-alert/OfferAlert.jsx
@@ -1,24 +1,98 @@
import React from 'react';
import PropTypes from 'prop-types';
+import {
+ FormattedMessage, FormattedDate, injectIntl, intlShape,
+} from '@edx/frontend-platform/i18n';
+import { Hyperlink } from '@edx/paragon';
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
+import messages from './messages';
-function OfferAlert({ payload }) {
+function OfferAlert({ intl, payload }) {
const {
- rawHtml,
+ offer,
+ userTimezone,
} = payload;
- return rawHtml && (
+
+ if (!offer) {
+ return null;
+ }
+
+ const {
+ code,
+ discountedPrice,
+ expirationDate,
+ originalPrice,
+ percentage,
+ upgradeUrl,
+ } = offer;
+ const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
+
+ const fullPricing = (
+ <>
+
+ {intl.formatMessage(messages.srPrices, { discountedPrice, originalPrice })}
+
+
+ {discountedPrice} {originalPrice}
+
+ >
+ );
+
+ return (
- {/* eslint-disable-next-line react/no-danger */}
-
+
+
+ ),
+ fullPricing,
+ percentage,
+ }}
+ />
+
+
+ {code}),
+ }}
+ />
+
+
+ {intl.formatMessage(messages.upgradeNow)}
+
);
}
OfferAlert.propTypes = {
+ intl: intlShape.isRequired,
payload: PropTypes.shape({
- rawHtml: PropTypes.string.isRequired,
+ offer: PropTypes.shape({
+ code: PropTypes.string.isRequired,
+ discountedPrice: PropTypes.string.isRequired,
+ expirationDate: PropTypes.string.isRequired,
+ originalPrice: PropTypes.string.isRequired,
+ percentage: PropTypes.number.isRequired,
+ upgradeUrl: PropTypes.string.isRequired,
+ }).isRequired,
+ userTimezone: PropTypes.string.isRequired,
}).isRequired,
};
-export default OfferAlert;
+export default injectIntl(OfferAlert);
diff --git a/src/alerts/offer-alert/hooks.js b/src/alerts/offer-alert/hooks.js
index 97f39509..5f4454a1 100644
--- a/src/alerts/offer-alert/hooks.js
+++ b/src/alerts/offer-alert/hooks.js
@@ -3,14 +3,13 @@ import { useAlert } from '../../generic/user-messages';
const OfferAlert = React.lazy(() => import('./OfferAlert'));
-export function useOfferAlert(offerHtml, topic) {
- const rawHtml = offerHtml || null;
- const isVisible = !!rawHtml; // if it exists, show it.
+export function useOfferAlert(offer, userTimezone, topic) {
+ const isVisible = !!offer; // if it exists, show it.
useAlert(isVisible, {
code: 'clientOfferAlert',
topic,
- payload: useMemo(() => ({ rawHtml }), [rawHtml]),
+ payload: useMemo(() => ({ offer, userTimezone }), [offer, userTimezone]),
});
return { clientOfferAlert: OfferAlert };
diff --git a/src/alerts/offer-alert/messages.js b/src/alerts/offer-alert/messages.js
new file mode 100644
index 00000000..f3e46123
--- /dev/null
+++ b/src/alerts/offer-alert/messages.js
@@ -0,0 +1,14 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ srPrices: {
+ id: 'learning.offer.screenReaderPrices',
+ defaultMessage: 'Original price: {originalPrice}, discount price: {discountedPrice}',
+ },
+ upgradeNow: {
+ id: 'learning.offer.upgradeNow',
+ defaultMessage: 'Upgrade now',
+ },
+});
+
+export default messages;
diff --git a/src/course-home/data/__factories__/outlineTabData.factory.js b/src/course-home/data/__factories__/outlineTabData.factory.js
index b3faf6df..cf19b16f 100644
--- a/src/course-home/data/__factories__/outlineTabData.factory.js
+++ b/src/course-home/data/__factories__/outlineTabData.factory.js
@@ -34,8 +34,8 @@ Factory.define('outlineTabData')
upgrade_url: `${host}/dashboard`,
}))
.attrs({
+ access_expiration: null,
can_show_upgrade_sock: true,
- course_expired_html: null,
course_goals: {
goal_options: [],
selected_goal: null,
@@ -50,6 +50,6 @@ Factory.define('outlineTabData')
extra_text: 'Contact the administrator.',
},
handouts_html: '
',
- offer_html: null,
+ offer: null,
welcome_message_html: 'Welcome to this course!
',
});
diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap
index 5ea68ed0..d34fecba 100644
--- a/src/course-home/data/__snapshots__/redux.test.js.snap
+++ b/src/course-home/data/__snapshots__/redux.test.js.snap
@@ -339,6 +339,7 @@ Object {
},
"outline": Object {
"course-v1:edX+DemoX+Demo_Course_1": Object {
+ "accessExpiration": null,
"canShowUpgradeSock": true,
"courseBlocks": Object {
"courses": Object {
@@ -375,7 +376,6 @@ Object {
},
},
},
- "courseExpiredHtml": null,
"courseGoals": Object {
"goalOptions": Array [],
"selectedGoal": null,
@@ -403,7 +403,7 @@ Object {
"handoutsHtml": "",
"hasEnded": undefined,
"id": "course-v1:edX+DemoX+Demo_Course_1",
- "offerHtml": null,
+ "offer": null,
"resumeCourse": Object {
"hasVisitedCourse": false,
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js
index c22bce0d..184b849a 100644
--- a/src/course-home/data/api.js
+++ b/src/course-home/data/api.js
@@ -141,33 +141,33 @@ export async function getOutlineTabData(courseId) {
const {
data,
} = tabData;
+ const accessExpiration = camelCaseObject(data.access_expiration);
const canShowUpgradeSock = data.can_show_upgrade_sock;
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
const courseGoals = camelCaseObject(data.course_goals);
- const courseExpiredHtml = data.course_expired_html;
const courseTools = camelCaseObject(data.course_tools);
const datesBannerInfo = camelCaseObject(data.dates_banner_info);
const datesWidget = camelCaseObject(data.dates_widget);
const enrollAlert = camelCaseObject(data.enroll_alert);
const handoutsHtml = data.handouts_html;
const hasEnded = data.has_ended;
- const offerHtml = data.offer_html;
+ const offer = camelCaseObject(data.offer);
const resumeCourse = camelCaseObject(data.resume_course);
const verifiedMode = camelCaseObject(data.verified_mode);
const welcomeMessageHtml = data.welcome_message_html;
return {
+ accessExpiration,
canShowUpgradeSock,
courseBlocks,
courseGoals,
- courseExpiredHtml,
courseTools,
datesBannerInfo,
datesWidget,
enrollAlert,
handoutsHtml,
hasEnded,
- offerHtml,
+ offer,
resumeCourse,
verifiedMode,
welcomeMessageHtml,
diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx
index 5fc4a95e..90aefe0b 100644
--- a/src/course-home/outline-tab/OutlineTab.jsx
+++ b/src/course-home/outline-tab/OutlineTab.jsx
@@ -43,6 +43,7 @@ function OutlineTab({ intl }) {
} = useModel('courses', courseId);
const {
+ accessExpiration,
canShowUpgradeSock,
courseBlocks: {
courses,
@@ -52,17 +53,17 @@ function OutlineTab({ intl }) {
goalOptions,
selectedGoal,
},
- courseExpiredHtml,
datesBannerInfo,
datesWidget: {
courseDateBlocks,
+ userTimezone,
},
hasEnded,
resumeCourse: {
hasVisitedCourse,
url: resumeCourseUrl,
},
- offerHtml,
+ offer,
verifiedMode,
} = useModel('outline', courseId);
@@ -75,8 +76,8 @@ function OutlineTab({ intl }) {
const enrollmentAlert = useEnrollmentAlert(courseId);
// Below the course title alerts (appearing in the order listed here)
- const offerAlert = useOfferAlert(offerHtml, 'outline-course-alerts');
- const accessExpirationAlert = useAccessExpirationAlert(courseExpiredHtml, 'outline-course-alerts');
+ const offerAlert = useOfferAlert(offer, userTimezone, 'outline-course-alerts');
+ const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, userTimezone, 'outline-course-alerts');
const courseStartAlert = useCourseStartAlert(courseId);
const courseEndAlert = useCourseEndAlert(courseId);
const certificateAvailableAlert = useCertificateAvailableAlert(courseId);
diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx
index c3d6998b..437e1253 100644
--- a/src/course-home/outline-tab/OutlineTab.test.jsx
+++ b/src/course-home/outline-tab/OutlineTab.test.jsx
@@ -342,11 +342,43 @@ describe('Outline Tab', () => {
});
describe('Access Expiration Alert', () => {
- // Appears if course_expired_html is provided
- it('appears', async () => {
- setTabData({ course_expired_html: 'Course Will Expire, Uh Oh
' });
+ it('has special masquerade text', async () => {
+ setTabData({
+ access_expiration: {
+ expiration_date: '2020-01-01T12:00:00Z',
+ masquerading_expired_course: true,
+ upgrade_deadline: null,
+ upgrade_url: null,
+ },
+ });
await fetchAndRender();
- await screen.findByText('Course Will Expire, Uh Oh');
+ await screen.findByText('This learner does not have access to this course.', { exact: false });
+ });
+
+ it('shows expiration', async () => {
+ setTabData({
+ access_expiration: {
+ expiration_date: '2080-01-01T12:00:00Z',
+ masquerading_expired_course: false,
+ upgrade_deadline: null,
+ upgrade_url: null,
+ },
+ });
+ await fetchAndRender();
+ await screen.findByText('Audit Access Expires');
+ });
+
+ it('shows upgrade prompt', async () => {
+ setTabData({
+ access_expiration: {
+ expiration_date: '2080-01-01T12:00:00Z',
+ masquerading_expired_course: false,
+ upgrade_deadline: '2070-01-01T12:00:00Z',
+ upgrade_url: 'https://example.com/upgrade',
+ },
+ });
+ await fetchAndRender();
+ await screen.findByText('to get unlimited access to the course as long as it exists on the site.', { exact: false });
});
});
diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx
index 63cf2f82..03ccc0d9 100644
--- a/src/courseware/course/Course.jsx
+++ b/src/courseware/course/Course.jsx
@@ -35,16 +35,17 @@ function Course({
].filter(element => element != null).map(element => element.title);
const {
+ accessExpiration,
canShowUpgradeSock,
celebrations,
- courseExpiredMessage,
- offerHtml,
+ offer,
+ userTimezone,
verifiedMode,
} = course;
// Below the tabs, above the breadcrumbs alerts (appearing in the order listed here)
- const offerAlert = useOfferAlert(offerHtml, 'course');
- const accessExpirationAlert = useAccessExpirationAlert(courseExpiredMessage, 'course');
+ const offerAlert = useOfferAlert(offer, userTimezone, 'course');
+ const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, userTimezone, 'course');
const dispatch = useDispatch();
const celebrateFirstSection = celebrations && celebrations.firstSection;
diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx
index 2b0faca5..dd9af222 100644
--- a/src/courseware/course/Course.test.jsx
+++ b/src/courseware/course/Course.test.jsx
@@ -85,23 +85,27 @@ describe('Course', () => {
});
it('displays offer and expiration alert', async () => {
- const offerText = 'test-offer';
- const offerId = `${offerText}-id`;
- const offerHtml = `${offerText}
`;
-
- const expirationText = 'test-expiration';
- const expirationId = `${expirationText}-id`;
- const expirationHtml = `${expirationText}
`;
-
const courseMetadata = Factory.build('courseMetadata', {
- offer_html: offerHtml,
- course_expired_message: expirationHtml,
+ access_expiration: {
+ expiration_date: '2080-01-01T12:00:00Z',
+ masquerading_expired_course: false,
+ upgrade_deadline: null,
+ upgrade_url: null,
+ },
+ offer: {
+ code: 'EDXWELCOME',
+ expiration_date: '2070-01-01T12:00:00Z',
+ original_price: '$100',
+ discounted_price: '$85',
+ percentage: 15,
+ upgrade_url: 'https://example.com/upgrade',
+ },
});
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
render(, { store: testStore });
- expect(await screen.findByTestId(offerId)).toHaveTextContent(offerText);
- expect(screen.getByTestId(expirationId)).toHaveTextContent(expirationText);
+ await screen.findByText('EDXWELCOME');
+ await screen.findByText('Audit Access Expires');
});
it('passes handlers to the sequence', async () => {
diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx
index f846fb00..92061270 100644
--- a/src/courseware/course/course-exit/CourseCelebration.jsx
+++ b/src/courseware/course/course-exit/CourseCelebration.jsx
@@ -218,6 +218,8 @@ function CourseCelebration({ intl }) {
} else {
footnote = ;
}
+ } else {
+ visitEvent = 'celebration_audit_no_upgrade';
}
break;
default:
diff --git a/src/courseware/data/api.js b/src/courseware/data/api.js
index ca5505f6..70345a09 100644
--- a/src/courseware/data/api.js
+++ b/src/courseware/data/api.js
@@ -110,14 +110,13 @@ function normalizeTabUrls(id, tabs) {
function normalizeMetadata(metadata) {
return {
+ accessExpiration: camelCaseObject(metadata.access_expiration),
canShowUpgradeSock: metadata.can_show_upgrade_sock,
contentTypeGatingEnabled: metadata.content_type_gating_enabled,
- // TODO: TNL-7185: return course expired _date_, instead of _message_
- courseExpiredMessage: metadata.course_expired_message,
id: metadata.id,
title: metadata.name,
number: metadata.number,
- offerHtml: metadata.offer_html,
+ offer: camelCaseObject(metadata.offer),
org: metadata.org,
enrollmentStart: metadata.enrollment_start,
enrollmentEnd: metadata.enrollment_end,
@@ -131,6 +130,7 @@ function normalizeMetadata(metadata) {
license: metadata.license,
verifiedMode: camelCaseObject(metadata.verified_mode),
tabs: normalizeTabUrls(metadata.id, camelCaseObject(metadata.tabs)),
+ userTimezone: metadata.user_timezone,
showCalculator: metadata.show_calculator,
notes: camelCaseObject(metadata.notes),
marketingUrl: metadata.marketing_url,