Skip to content

Commit

Permalink
feat: Add past expiration messaging for UpgradeNotification (openedx#853
Browse files Browse the repository at this point in the history
)

REV-2500
  • Loading branch information
julianajlk authored Mar 11, 2022
1 parent 4f1a50e commit 1d3a779
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Factory.define('upgradeNotificationData')
.option('dateBlocks', [])
.option('offer', null)
.option('userTimezone', null)
.option('accessExpiration', null)
.option('contentTypeGatingEnabled', false)
.attr('courseId', 'course-v1:edX+DemoX+Demo_Course')
.attr('upsellPageName', 'test')
Expand All @@ -18,4 +17,9 @@ Factory.define('upgradeNotificationData')
upgradeUrl: `${host}/dashboard`,
}))
.attr('org', 'edX')
.attrs({
accessExpiration: {
expiration_date: '1950-07-13T02:04:49.040006Z',
},
})
.attr('timeOffsetMillis', 0);
5 changes: 5 additions & 0 deletions src/course-home/outline-tab/OutlineTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ function OutlineTab({ intl }) {
verifiedMode,
} = useModel('outline', courseId);

const {
marketingUrl,
} = useModel('coursewareMeta', courseId);

const [expandAll, setExpandAll] = useState(false);

const eventProperties = {
Expand Down Expand Up @@ -190,6 +194,7 @@ function OutlineTab({ intl }) {
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function NotificationTray({ intl }) {
const {
accessExpiration,
contentTypeGatingEnabled,
marketingUrl,
offer,
org,
timeOffsetMillis,
Expand All @@ -45,6 +46,7 @@ function NotificationTray({ intl }) {
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="in_course"
userTimezone={userTimezone}
shouldDisplayBorder={false}
Expand Down
135 changes: 122 additions & 13 deletions src/generic/upgrade-notification/UpgradeNotification.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { FormattedDate, FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { setLocalStorage } from '../../data/localStorage';
import { UpgradeButton } from '../upgrade-button';
import {
Expand Down Expand Up @@ -95,11 +96,24 @@ UpsellFBESoonCardContent.defaultProps = {
timezoneFormatArgs: {},
};

function PastExpirationCardContent() {
return (
<div className="upgrade-notification-text">
<p>
<FormattedMessage
id="learning.generic.upgradeNotification.pastExpiration.content"
defaultMessage="The upgrade deadline for this course passed. To upgrade, enroll in the next available session."
/>
</p>
</div>
);
}

function ExpirationCountdown({
courseId, hoursToExpiration, setupgradeNotificationCurrentState, type,
}) {
let expirationText;
if (hoursToExpiration >= 24) {
if (hoursToExpiration >= 24) { // More than 1 day left
// setupgradeNotificationCurrentState is available in NotificationTray (not course home)
if (setupgradeNotificationCurrentState) {
if (type === 'access') {
Expand All @@ -122,7 +136,7 @@ function ExpirationCountdown({
}}
/>
);
} else if (hoursToExpiration >= 1) {
} else if (hoursToExpiration >= 1) { // More than 1 hour left
// setupgradeNotificationCurrentState is available in NotificationTray (not course home)
if (setupgradeNotificationCurrentState) {
if (type === 'access') {
Expand All @@ -145,7 +159,7 @@ function ExpirationCountdown({
}}
/>
);
} else {
} else { // Less than 1 hour
// setupgradeNotificationCurrentState is available in NotificationTray (not course home)
if (setupgradeNotificationCurrentState) {
if (type === 'access') {
Expand Down Expand Up @@ -220,9 +234,52 @@ AccessExpirationDateBanner.defaultProps = {
setupgradeNotificationCurrentState: null,
};

function PastExpirationDateBanner({
courseId, accessExpirationDate, timezoneFormatArgs, setupgradeNotificationCurrentState,
}) {
if (setupgradeNotificationCurrentState) {
setupgradeNotificationCurrentState('PastExpirationDate');
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'PastExpirationDate');
}
return (
<div className="upsell-warning">
<FormattedMessage
id="learning.generic.upgradeNotification.pastExpiration.banner"
defaultMessage="Upgrade deadline passed on {date}"
values={{
date: (
<FormattedDate
key="accessExpireDate"
day="numeric"
month="long"
value={accessExpirationDate}
{...timezoneFormatArgs}
/>
),
}}
/>
</div>
);
}

PastExpirationDateBanner.propTypes = {
courseId: PropTypes.string.isRequired,
accessExpirationDate: PropTypes.PropTypes.instanceOf(Date).isRequired,
timezoneFormatArgs: PropTypes.shape({
timeZone: PropTypes.string,
}),
setupgradeNotificationCurrentState: PropTypes.func,
};

PastExpirationDateBanner.defaultProps = {
timezoneFormatArgs: {},
setupgradeNotificationCurrentState: null,
};

function UpgradeNotification({
accessExpiration,
contentTypeGatingEnabled,
marketingUrl,
courseId,
offer,
org,
Expand All @@ -233,8 +290,11 @@ function UpgradeNotification({
userTimezone,
verifiedMode,
}) {
const dateNow = Date.now();
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
const correctedTime = new Date(Date.now() + timeOffsetMillis);
const correctedTime = new Date(dateNow + timeOffsetMillis);
const accessExpirationDate = accessExpiration ? new Date(accessExpiration.expirationDate) : null;
const pastExpirationDeadline = accessExpiration ? new Date(dateNow) > accessExpirationDate : false;

if (!verifiedMode) {
return null;
Expand Down Expand Up @@ -274,20 +334,31 @@ function UpgradeNotification({
});
};

const logClickPastExpiration = () => {
sendTrackEvent('edx.bi.ecommerce.upgrade_notification.past_expiration.button_clicked', {
...eventProperties,
linkCategory: 'upgrade_notification',
linkName: `${upsellPageName}_course_details`,
linkType: 'button',
pageName: upsellPageName,
});
};

/*
There are 4 parts that change in the upgrade card:
There are 5 parts that change in the upgrade card:
upgradeNotificationHeaderText
expirationBanner
upsellMessage
callToActionButton
offerCode
*/
let upgradeNotificationHeaderText;
let expirationBanner;
let upsellMessage;
let callToActionButton;
let offerCode;

if (!!accessExpiration && !!contentTypeGatingEnabled) {
const accessExpirationDate = new Date(accessExpiration.expirationDate);
const hoursToAccessExpiration = Math.floor((accessExpirationDate - correctedTime) / 1000 / 60 / 60);

if (hoursToAccessExpiration >= (7 * 24)) {
Expand Down Expand Up @@ -327,7 +398,8 @@ function UpgradeNotification({
);
}
upsellMessage = <UpsellFBEFarAwayCardContent />;
} else { // more urgent messaging if there's less than 7 days left to access expiration
} else if (hoursToAccessExpiration < (7 * 24) && hoursToAccessExpiration >= 0) {
// more urgent messaging if there's less than 7 days left to access expiration
upgradeNotificationHeaderText = (
<FormattedMessage
id="learning.generic.upgradeNotification.accessExpirationUrgent"
Expand All @@ -348,6 +420,24 @@ function UpgradeNotification({
timezoneFormatArgs={timezoneFormatArgs}
/>
);
} else { // access expiration deadline has passed
upgradeNotificationHeaderText = (
<FormattedMessage
id="learning.generic.upgradeNotification.accessExpirationPast"
defaultMessage="Course Access Expiration"
/>
);
expirationBanner = (
<PastExpirationDateBanner
courseId={courseId}
accessExpirationDate={accessExpirationDate}
timezoneFormatArgs={timezoneFormatArgs}
setupgradeNotificationCurrentState={setupgradeNotificationCurrentState}
/>
);
upsellMessage = (
<PastExpirationCardContent />
);
}
} else { // FBE is turned off
upgradeNotificationHeaderText = (
Expand All @@ -359,6 +449,28 @@ function UpgradeNotification({
upsellMessage = (<UpsellNoFBECardContent />);
}

if (pastExpirationDeadline) {
callToActionButton = (
<Button
variant="primary"
onClick={logClickPastExpiration}
href={marketingUrl}
block
>
View Course Details
</Button>
);
} else {
callToActionButton = (
<UpgradeButton
offer={offer}
onClick={logClick}
verifiedMode={verifiedMode}
block
/>
);
}

if (offer) { // if there's a first purchase discount, message the code at the bottom
offerCode = (
<div className="text-center discount-info">
Expand All @@ -384,12 +496,7 @@ function UpgradeNotification({
{upsellMessage}
</div>
<div className="upgrade-notification-button">
<UpgradeButton
offer={offer}
onClick={logClick}
verifiedMode={verifiedMode}
block
/>
{callToActionButton}
</div>
{offerCode}
</div>
Expand All @@ -404,6 +511,7 @@ UpgradeNotification.propTypes = {
expirationDate: PropTypes.string,
}),
contentTypeGatingEnabled: PropTypes.bool,
marketingUrl: PropTypes.string,
offer: PropTypes.shape({
expirationDate: PropTypes.string,
percentage: PropTypes.number,
Expand All @@ -424,6 +532,7 @@ UpgradeNotification.propTypes = {
UpgradeNotification.defaultProps = {
accessExpiration: null,
contentTypeGatingEnabled: false,
marketingUrl: null,
offer: null,
setupgradeNotificationCurrentState: null,
shouldDisplayBorder: null,
Expand Down
41 changes: 41 additions & 0 deletions src/generic/upgrade-notification/UpgradeNotification.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('Upgrade Notification', () => {
it('renders non-FBE when there is a verified mode and content gating, but no access expiration', async () => {
buildAndRender({
contentTypeGatingEnabled: true,
accessExpiration: null,
});
expect(screen.getByRole('heading', { name: 'Pursue a verified certificate' })).toBeInTheDocument();
expect(screen.getByText(/Earn a.*?of completion to showcase on your resumé/s).textContent).toMatch('Earn a verified certificate of completion to showcase on your resumé');
Expand Down Expand Up @@ -278,4 +279,44 @@ describe('Upgrade Notification', () => {
expect(screen.getByText(/Upgrade for/).textContent).toMatch('$126.65 ($149)');
expect(screen.getByText(/Use code.*?at checkout/s).textContent).toMatch('Use code Welcome15 at checkout');
});

it('renders past access expiration message properly', async () => {
const expirationDate = new Date(dateNow);
expirationDate.setDate(expirationDate.getDate() - 1);
buildAndRender({
contentTypeGatingEnabled: true,
accessExpiration: {
expirationDate: expirationDate.toString(),
},
});
expect(screen.getByRole('heading', { name: 'Course Access Expiration' })).toBeInTheDocument();
expect(screen.getByText(/The upgrade deadline/s).textContent).toMatch('The upgrade deadline for this course passed');
expect(screen.getByText(/To upgrade/s).textContent).toMatch('To upgrade, enroll in the next available session');
expect(screen.getByRole('button', { name: 'View Course Details' })).toBeInTheDocument();
});

it('sends course details click info to segment if past access expiration', async () => {
const expirationDate = new Date(dateNow);
expirationDate.setDate(expirationDate.getDate() - 1);
sendTrackEvent.mockClear();
buildAndRender({
pageName: 'test',
contentTypeGatingEnabled: true,
accessExpiration: {
expirationDate: expirationDate.toString(),
},
});

const courseDetailsLink = await waitFor(() => screen.queryByRole('button', { name: 'View Course Details' }));
fireEvent.click(courseDetailsLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upgrade_notification.past_expiration.button_clicked', {
org_key: 'edX',
courserun_key: 'course-v1:edX+DemoX+Demo_Course',
linkCategory: 'upgrade_notification',
linkName: 'test_course_details',
linkType: 'button',
pageName: 'test',
});
});
});

0 comments on commit 1d3a779

Please sign in to comment.