Compare commits

...

4 Commits

Author SHA1 Message Date
Zacharis278
397f6a77fd feat: adding more code needed by lockpaywall 2024-07-29 10:48:58 -04:00
Zacharis278
30c6b23766 feat: add upsell bullets back in, required by lock paywall 2024-07-29 10:48:58 -04:00
Zacharis278
58cbed25d7 feat: undo remove lockpaywall 2024-07-29 10:48:58 -04:00
Zacharis278
73de57ddf4 feat: removes sidebar upgrade and fbe paywall 2024-07-29 10:48:58 -04:00
10 changed files with 18 additions and 1127 deletions

View File

@@ -17,7 +17,6 @@ import { fetchOutlineTab } from '../data';
import messages from './messages';
import Section from './Section';
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
import useCourseEndAlert from './alerts/course-end-alert';
@@ -39,11 +38,9 @@ const OutlineTab = ({ intl }) => {
isSelfPaced,
org,
title,
userTimezone,
} = useModel('courseHomeMeta', courseId);
const {
accessExpiration,
courseBlocks: {
courses,
sections,
@@ -52,20 +49,12 @@ const OutlineTab = ({ intl }) => {
selectedGoal,
weeklyLearningGoalEnabled,
} = {},
datesBannerInfo,
datesWidget: {
courseDateBlocks,
},
enableProctoredExams,
offer,
timeOffsetMillis,
verifiedMode,
} = useModel('outline', courseId);
const {
marketingUrl,
} = useModel('coursewareMeta', courseId);
const [expandAll, setExpandAll] = useState(false);
const navigate = useNavigate();
@@ -198,21 +187,7 @@ const OutlineTab = ({ intl }) => {
<PluginSlot
id="outline_tab_notifications_slot"
pluginProps={{ courseId }}
>
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
/>
</PluginSlot>
/>
<CourseDates />
<CourseHandouts />
</div>

View File

@@ -5,7 +5,7 @@ import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';
import Cookies from 'js-cookie';
@@ -1176,80 +1176,6 @@ describe('Outline Tab', () => {
});
});
describe('Upgrade Card', () => {
it('renders title when upgrade is available', async () => {
await fetchAndRender();
expect(screen.queryByRole('heading', { name: 'Pursue a verified certificate' })).toBeInTheDocument();
});
it('displays link to upgrade', async () => {
await fetchAndRender();
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
it('viewing upgrade card sends analytics', async () => {
sendTrackEvent.mockClear();
sendTrackingLogEvent.mockClear();
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('Promotion Viewed', {
org_key: 'edX',
courserun_key: courseId,
creative: 'sidebarupsell',
name: 'In-Course Verification Prompt',
position: 'sidebar-message',
promotion_id: 'courseware_verified_certificate_upsell',
});
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(1);
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.displayed', {
org_key: 'edX',
courserun_key: courseId,
});
});
it('clicking upgrade link sends analytics', async () => {
await fetchAndRender();
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
sendTrackEvent.mockClear();
sendTrackingLogEvent.mockClear();
const upgradeButton = screen.getByRole('link', { name: 'Upgrade for $149' });
fireEvent.click(upgradeButton);
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'Promotion Clicked', {
org_key: 'edX',
courserun_key: courseId,
creative: 'sidebarupsell',
name: 'In-Course Verification Prompt',
position: 'sidebar-message',
promotion_id: 'courseware_verified_certificate_upsell',
});
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upsell_links_clicked', {
org_key: 'edX',
courserun_key: courseId,
linkCategory: 'green_upgrade',
linkName: 'course_home_green',
linkType: 'button',
pageName: 'course_home',
});
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(2);
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(1, 'edx.bi.course.upgrade.sidebarupsell.clicked', {
org_key: 'edX',
courserun_key: courseId,
});
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.course.enrollment.upgrade.clicked', {
org_key: 'edX',
courserun_key: courseId,
location: 'sidebar-message',
});
});
});
describe('Account Activation Alert', () => {
beforeEach(() => {
const intersectionObserverMock = () => ({

View File

@@ -3,7 +3,6 @@ import React, { useContext, useEffect, useMemo } from 'react';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useModel } from '../../../../../../generic/model-store';
import UpgradeNotification from '../../../../../../generic/upgrade-notification/UpgradeNotification';
import { WIDGETS } from '../../../../../../constants';
import SidebarContext from '../../../SidebarContext';
@@ -21,17 +20,11 @@ const NotificationsWidget = () => {
const course = useModel('coursewareMeta', courseId);
const {
accessExpiration,
contentTypeGatingEnabled,
end,
enrollmentEnd,
enrollmentMode,
enrollmentStart,
marketingUrl,
offer,
start,
timeOffsetMillis,
userTimezone,
verificationStatus,
} = course;
@@ -81,24 +74,7 @@ const NotificationsWidget = () => {
setNotificationCurrentState: setUpgradeNotificationCurrentState,
toggleSidebar: onToggleSidebar,
}}
>
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="in_course"
userTimezone={userTimezone}
shouldDisplayBorder={false}
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
upgradeNotificationCurrentState={upgradeNotificationCurrentState}
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
toggleSidebar={onToggleSidebar}
/>
</PluginSlot>
/>
</div>
);
};

View File

@@ -18,9 +18,22 @@ import SidebarContext from '../../../SidebarContext';
import NotificationsWidget from './NotificationsWidget';
import setupDiscussionSidebar from '../../../../test-utils';
initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
/* eslint-disable react/prop-types */
jest.mock('@openedx/frontend-plugin-framework', () => ({
...jest.requireActual('@openedx/frontend-plugin-framework'),
Plugin: () => 'Plugin',
PluginSlot: ({ id, pluginProps }) => (
<div data-testid={id}>
<button type="button" onClick={pluginProps?.toggleSidebar}>Close</button>
PluginSlot_{id}
</div>
),
}));
initializeMockApp();
describe('NotificationsWidget', () => {
let axiosMock;
let store;
@@ -93,27 +106,6 @@ describe('NotificationsWidget', () => {
expect(screen.getByTestId('notification_widget_slot')).toBeInTheDocument();
});
it('renders upgrade card', async () => {
await fetchAndRender(
<SidebarContext.Provider value={{
currentSidebar: ID,
courseId,
hideNotificationbar: false,
isNotificationbarAvailable: true,
}}
>
<NotificationsWidget />
</SidebarContext.Provider>,
);
// The Upgrade Notification should be inside the PluginSlot.
const UpgradeNotification = document.querySelector('.upgrade-notification');
expect(UpgradeNotification).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
expect(screen.queryByText('You have no new notifications at this time.')).not.toBeInTheDocument();
});
it('renders no notifications bar if no verified mode', async () => {
setMetadata({ verified_mode: null });
await fetchAndRender(

View File

@@ -5,7 +5,6 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useModel } from '@src/generic/model-store';
import UpgradeNotification from '../../../../../generic/upgrade-notification/UpgradeNotification';
import messages from '../../../messages';
import SidebarBase from '../../common/SidebarBase';
@@ -23,17 +22,11 @@ const NotificationTray = ({ intl }) => {
const course = useModel('coursewareMeta', courseId);
const {
accessExpiration,
contentTypeGatingEnabled,
end,
enrollmentEnd,
enrollmentMode,
enrollmentStart,
marketingUrl,
offer,
start,
timeOffsetMillis,
userTimezone,
verificationStatus,
} = course;
@@ -88,23 +81,7 @@ const NotificationTray = ({ intl }) => {
notificationCurrentState: upgradeNotificationCurrentState,
setNotificationCurrentState: setUpgradeNotificationCurrentState,
}}
>
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="in_course"
userTimezone={userTimezone}
shouldDisplayBorder={false}
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
upgradeNotificationCurrentState={upgradeNotificationCurrentState}
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
/>
</PluginSlot>
/>
) : (
<p className="p-3 small">{intl.formatMessage(messages.noNotificationsMessage)}</p>
)}

View File

@@ -94,26 +94,6 @@ describe('NotificationTray', () => {
expect(screen.getByTestId('notification_tray_slot')).toBeInTheDocument();
});
it('renders upgrade card', async () => {
await fetchAndRender(
<SidebarContext.Provider value={{
currentSidebar: ID,
courseId,
}}
>
<NotificationTray />
</SidebarContext.Provider>,
);
expect(document.querySelector('.upgrade-notification')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Upgrade for $149' }))
.toBeInTheDocument();
expect(screen.queryByText('You have no new notifications at this time.'))
.not
.toBeInTheDocument();
});
it('renders no notifications message if no verified mode', async () => {
setMetadata({ verified_mode: null });
await fetchAndRender(

View File

@@ -1,564 +0,0 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
useIntl, FormattedDate, FormattedMessage, injectIntl,
} from '@edx/frontend-platform/i18n';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { Button, Icon, IconButton } from '@openedx/paragon';
import { Close } from '@openedx/paragon/icons';
import { setLocalStorage } from '../../data/localStorage';
import { UpgradeButton } from '../upgrade-button';
import {
VerifiedCertBullet,
UnlockGradedBullet,
FullAccessBullet,
SupportMissionBullet,
} from '../upsell-bullets/UpsellBullets';
import messages from '../messages';
const UpsellNoFBECardContent = () => (
<ul className="fa-ul upgrade-notification-ul pt-0">
<VerifiedCertBullet />
<SupportMissionBullet />
</ul>
);
const UpsellFBEFarAwayCardContent = () => (
<ul className="fa-ul upgrade-notification-ul">
<VerifiedCertBullet />
<UnlockGradedBullet />
<FullAccessBullet />
<SupportMissionBullet />
</ul>
);
const UpsellFBESoonCardContent = ({ accessExpirationDate, timezoneFormatArgs }) => {
const includingAnyProgress = (
<span className="font-weight-bold">
<FormattedMessage
id="learning.generic.upgradeNotification.expirationAccessLoss.progress"
defaultMessage="including any progress"
/>
</span>
);
const date = (
<FormattedDate
key="accessDate"
day="numeric"
month="long"
value={new Date(accessExpirationDate)}
{...timezoneFormatArgs}
/>
);
const benefitsOfUpgrading = (
<a className="inline-link-underline font-weight-bold" rel="noopener noreferrer" target="_blank" href="https://support.edx.org/hc/en-us/articles/360013426573-What-are-the-differences-between-audit-free-and-verified-paid-courses-">
<FormattedMessage
id="learning.generic.upgradeNotification.expirationVerifiedCert.benefits"
defaultMessage="benefits of upgrading"
/>
</a>
);
return (
<div className="upgrade-notification-text">
<p>
<FormattedMessage
id="learning.generic.upgradeNotification.expirationAccessLoss"
defaultMessage="You will lose all access to this course, {includingAnyProgress}, on {date}."
values={{
includingAnyProgress,
date,
}}
/>
</p>
<p>
<FormattedMessage
id="learning.generic.upgradeNotification.expirationVerifiedCert"
defaultMessage="Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}."
values={{ benefitsOfUpgrading }}
/>
</p>
</div>
);
};
UpsellFBESoonCardContent.propTypes = {
accessExpirationDate: PropTypes.PropTypes.instanceOf(Date).isRequired,
timezoneFormatArgs: PropTypes.shape({
timeZone: PropTypes.string,
}),
};
UpsellFBESoonCardContent.defaultProps = {
timezoneFormatArgs: {},
};
const PastExpirationCardContent = () => (
<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>
);
const ExpirationCountdown = ({
courseId, hoursToExpiration, setupgradeNotificationCurrentState, type,
}) => {
let expirationText;
if (hoursToExpiration >= 24) { // More than 1 day left
// setupgradeNotificationCurrentState is available in NotificationTray (not course home)
if (setupgradeNotificationCurrentState) {
if (type === 'access') {
setupgradeNotificationCurrentState('accessDaysLeft');
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'accessDaysLeft');
}
if (type === 'offer') {
setupgradeNotificationCurrentState('FPDdaysLeft');
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'FPDdaysLeft');
}
}
expirationText = (
<FormattedMessage
id="learning.generic.upgradeNotification.expirationDays"
defaultMessage={`{dayCount, number} {dayCount, plural,
one {day}
other {days}} left`}
values={{
dayCount: (Math.floor(hoursToExpiration / 24)),
}}
/>
);
} else if (hoursToExpiration >= 1) { // More than 1 hour left
// setupgradeNotificationCurrentState is available in NotificationTray (not course home)
if (setupgradeNotificationCurrentState) {
if (type === 'access') {
setupgradeNotificationCurrentState('accessHoursLeft');
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'accessHoursLeft');
}
if (type === 'offer') {
setupgradeNotificationCurrentState('FPDHoursLeft');
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'FPDHoursLeft');
}
}
expirationText = (
<FormattedMessage
id="learning.generic.upgradeNotification.expirationHours"
defaultMessage={`{hourCount, number} {hourCount, plural,
one {hour}
other {hours}} left`}
values={{
hourCount: (hoursToExpiration),
}}
/>
);
} else { // Less than 1 hour
// setupgradeNotificationCurrentState is available in NotificationTray (not course home)
if (setupgradeNotificationCurrentState) {
if (type === 'access') {
setupgradeNotificationCurrentState('accessLastHour');
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'accessLastHour');
}
if (type === 'offer') {
setupgradeNotificationCurrentState('FPDLastHour');
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'FPDLastHour');
}
}
expirationText = (
<FormattedMessage
id="learning.generic.upgradeNotification.expirationMinutes"
defaultMessage="Less than 1 hour left"
/>
);
}
return (<div className="upsell-warning">{expirationText}</div>);
};
ExpirationCountdown.propTypes = {
courseId: PropTypes.string.isRequired,
hoursToExpiration: PropTypes.number.isRequired,
setupgradeNotificationCurrentState: PropTypes.func,
type: PropTypes.string,
};
ExpirationCountdown.defaultProps = {
setupgradeNotificationCurrentState: null,
type: null,
};
const AccessExpirationDateBanner = ({
courseId, accessExpirationDate, timezoneFormatArgs, setupgradeNotificationCurrentState,
}) => {
if (setupgradeNotificationCurrentState) {
setupgradeNotificationCurrentState('accessDateView');
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'accessDateView');
}
return (
<div className="upsell-warning-light">
<FormattedMessage
id="learning.generic.upgradeNotification.expiration"
defaultMessage="Course access will expire {date}"
values={{
date: (
<FormattedDate
key="accessExpireDate"
day="numeric"
month="long"
value={accessExpirationDate}
{...timezoneFormatArgs}
/>
),
}}
/>
</div>
);
};
AccessExpirationDateBanner.propTypes = {
courseId: PropTypes.string.isRequired,
accessExpirationDate: PropTypes.PropTypes.instanceOf(Date).isRequired,
timezoneFormatArgs: PropTypes.shape({
timeZone: PropTypes.string,
}),
setupgradeNotificationCurrentState: PropTypes.func,
};
AccessExpirationDateBanner.defaultProps = {
timezoneFormatArgs: {},
setupgradeNotificationCurrentState: null,
};
const 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,
};
const UpgradeNotification = ({
accessExpiration,
contentTypeGatingEnabled,
marketingUrl,
courseId,
offer,
org,
setupgradeNotificationCurrentState,
shouldDisplayBorder,
timeOffsetMillis,
upsellPageName,
userTimezone,
verifiedMode,
toggleSidebar,
}) => {
const intl = useIntl();
const dateNow = Date.now();
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
const correctedTime = new Date(dateNow + timeOffsetMillis);
const accessExpirationDate = accessExpiration ? new Date(accessExpiration.expirationDate) : null;
const pastExpirationDeadline = accessExpiration ? new Date(dateNow) > accessExpirationDate : false;
const eventProperties = {
org_key: org,
courserun_key: courseId,
};
const promotionEventProperties = {
creative: 'sidebarupsell',
name: 'In-Course Verification Prompt',
position: 'sidebar-message',
promotion_id: 'courseware_verified_certificate_upsell',
...eventProperties,
};
useEffect(() => {
sendTrackingLogEvent('edx.bi.course.upgrade.sidebarupsell.displayed', eventProperties);
sendTrackEvent('Promotion Viewed', promotionEventProperties);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!verifiedMode) {
return null;
}
const logClick = () => {
sendTrackingLogEvent('edx.bi.course.upgrade.sidebarupsell.clicked', eventProperties);
sendTrackingLogEvent('edx.course.enrollment.upgrade.clicked', {
...eventProperties,
location: 'sidebar-message',
});
sendTrackEvent('Promotion Clicked', promotionEventProperties);
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
...eventProperties,
linkCategory: 'green_upgrade',
linkName: `${upsellPageName}_green`,
linkType: 'button',
pageName: upsellPageName,
});
};
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 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 hoursToAccessExpiration = Math.floor((accessExpirationDate - correctedTime) / 1000 / 60 / 60);
if (hoursToAccessExpiration >= (7 * 24)) {
if (offer) { // countdown to the first purchase discount if there is one
const hoursToDiscountExpiration = Math.floor((new Date(offer.expirationDate) - correctedTime) / 1000 / 60 / 60);
upgradeNotificationHeaderText = (
<FormattedMessage
id="learning.generic.upgradeNotification.firstTimeLearnerDiscount"
defaultMessage="{percentage}% First-Time Learner Discount"
values={{
percentage: (offer.percentage),
}}
/>
);
expirationBanner = (
<ExpirationCountdown
courseId={courseId}
hoursToExpiration={hoursToDiscountExpiration}
setupgradeNotificationCurrentState={setupgradeNotificationCurrentState}
type="offer"
/>
);
} else {
upgradeNotificationHeaderText = (
<FormattedMessage
id="learning.generic.upgradeNotification.accessExpiration"
defaultMessage="Upgrade your course today"
/>
);
expirationBanner = (
<AccessExpirationDateBanner
courseId={courseId}
accessExpirationDate={accessExpirationDate}
timezoneFormatArgs={timezoneFormatArgs}
setupgradeNotificationCurrentState={setupgradeNotificationCurrentState}
/>
);
}
upsellMessage = <UpsellFBEFarAwayCardContent />;
} 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"
defaultMessage="Course Access Expiration"
/>
);
expirationBanner = (
<ExpirationCountdown
courseId={courseId}
hoursToExpiration={hoursToAccessExpiration}
setupgradeNotificationCurrentState={setupgradeNotificationCurrentState}
type="access"
/>
);
upsellMessage = (
<UpsellFBESoonCardContent
accessExpirationDate={accessExpirationDate}
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 = (
<FormattedMessage
id="learning.generic.upgradeNotification.pursueAverifiedCertificate"
defaultMessage="Pursue a verified certificate"
/>
);
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">
<FormattedMessage
id="learning.generic.upgradeNotification.code"
defaultMessage="Use code {code} at checkout"
values={{
code: (<span className="font-weight-bold">{offer.code}</span>),
}}
/>
</div>
);
}
return (
<section className={classNames('upgrade-notification small', { 'card mb-4': shouldDisplayBorder })}>
<div id="courseHome-upgradeNotification">
<h2
className={classNames('h5 upgrade-notification-header', {
'd-flex align-items-center mr-2 ml-4 my-1.5 font-size-18': !!toggleSidebar,
})}
id="outline-sidebar-upgrade-header"
>
{upgradeNotificationHeaderText}
{!!toggleSidebar && (
<div className="d-inline-flex ml-auto">
<IconButton
src={Close}
size="sm"
iconAs={Icon}
onClick={toggleSidebar}
className="icon-hover"
alt={intl.formatMessage(messages.close)}
/>
</div>
)}
</h2>
{expirationBanner}
<div className="upgrade-notification-message">
{upsellMessage}
</div>
<div className="upgrade-notification-button">
{callToActionButton}
</div>
{offerCode}
</div>
</section>
);
};
UpgradeNotification.propTypes = {
courseId: PropTypes.string.isRequired,
org: PropTypes.string.isRequired,
accessExpiration: PropTypes.shape({
expirationDate: PropTypes.string,
}),
contentTypeGatingEnabled: PropTypes.bool,
marketingUrl: PropTypes.string,
offer: PropTypes.shape({
expirationDate: PropTypes.string,
percentage: PropTypes.number,
code: PropTypes.string,
}),
toggleSidebar: PropTypes.func,
shouldDisplayBorder: PropTypes.bool,
setupgradeNotificationCurrentState: PropTypes.func,
timeOffsetMillis: PropTypes.number,
upsellPageName: PropTypes.string.isRequired,
userTimezone: PropTypes.string,
verifiedMode: PropTypes.shape({
currencySymbol: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
upgradeUrl: PropTypes.string.isRequired,
}),
};
UpgradeNotification.defaultProps = {
accessExpiration: null,
contentTypeGatingEnabled: false,
marketingUrl: null,
offer: null,
setupgradeNotificationCurrentState: null,
shouldDisplayBorder: null,
timeOffsetMillis: 0,
userTimezone: null,
verifiedMode: null,
toggleSidebar: null,
};
export default injectIntl(UpgradeNotification);

View File

@@ -1,46 +0,0 @@
.upgrade-notification {
border-radius: 0 !important;
}
.upgrade-notification-header {
margin: 1.25rem;
}
.upsell-warning {
background-color: $danger-100;
}
.upsell-warning-light {
background-color: $warning-100;
}
.upsell-warning, .upsell-warning-light {
padding: 0.5rem 1.25rem;
}
// .fa-ul added so specificity is higher than Font Awesome's .fa-ul.
// An additional Font Awesome stylesheet is imported by Braze in
// stage/production but not devstack.
.upgrade-notification-ul.fa-ul {
padding: 0.875rem 1.25rem 0;
margin: 0 0 1rem 2.5rem;
}
.upgrade-notification-text {
padding: 0.875rem 1.25rem 0 1.25rem;
}
.upgrade-notification-button {
padding: 1.25rem;
padding-top: 0;
}
.discount-info {
border-top: 1px solid $light-400;
padding-top: .75rem;
padding-bottom: .75rem;
}
.font-size-18 {
font-size: 18px !important;
}

View File

@@ -1,322 +0,0 @@
import React from 'react';
import { Factory } from 'rosie';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
fireEvent,
initializeMockApp,
render,
screen,
waitFor,
} from '../../setupTest';
import UpgradeNotification from './UpgradeNotification';
initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
const dateNow = new Date('2021-04-13T11:01:58.000Z');
jest
.spyOn(global.Date, 'now')
.mockImplementation(() => dateNow.valueOf());
describe('Upgrade Notification', () => {
function buildAndRender(attributes) {
const upgradeNotificationData = Factory.build('upgradeNotificationData', { ...attributes });
render(<UpgradeNotification {...upgradeNotificationData} />);
}
it('sends upgrade click info to segment', async () => {
sendTrackEvent.mockClear();
buildAndRender({ pageName: 'test' });
const upgradeButton = await waitFor(() => screen.queryByRole('link', { name: 'Upgrade for $149' }));
fireEvent.click(upgradeButton);
expect(sendTrackEvent).toHaveBeenCalledTimes(3);
expect(sendTrackEvent).toHaveBeenNthCalledWith(3, 'edx.bi.ecommerce.upsell_links_clicked', {
org_key: 'edX',
courserun_key: 'course-v1:edX+DemoX+Demo_Course',
linkCategory: 'green_upgrade',
linkName: 'test_green',
linkType: 'button',
pageName: 'test',
});
});
it('does not render when there is no verified mode', async () => {
buildAndRender({ verifiedMode: null });
expect(screen.queryByRole('link', { name: 'Upgrade for $149' })).not.toBeInTheDocument();
});
it('renders non-FBE when there is a verified mode but no FBE', async () => {
buildAndRender();
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é');
expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our mission at edX');
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
it('renders non-FBE when there is a verified mode and access expiration, but no content gating', async () => {
const expirationDate = new Date(dateNow);
expirationDate.setMinutes(expirationDate.getMinutes() + 45);
buildAndRender({
accessExpiration: {
expirationDate: expirationDate.toString(),
},
});
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é');
expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our mission at edX');
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
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é');
expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our mission at edX');
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
it('renders non-FBE with a discount properly', async () => {
const discountExpirationDate = new Date(dateNow);
discountExpirationDate.setDate(discountExpirationDate.getDate() + 6);
buildAndRender({
offer: {
expirationDate: discountExpirationDate.toString(),
percentage: 15,
code: 'Welcome15',
discountedPrice: '$126.65',
originalPrice: '$149',
upgradeUrl: 'www.exampleUpgradeUrl.com',
},
});
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é');
expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our mission at edX');
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 FBE expiration within an hour properly', async () => {
const expirationDate = new Date(dateNow);
expirationDate.setMinutes(expirationDate.getMinutes() + 45);
buildAndRender({
accessExpiration: {
expirationDate: expirationDate.toString(),
},
contentTypeGatingEnabled: true,
});
expect(screen.getByRole('heading', { name: 'Course Access Expiration' })).toBeInTheDocument();
expect(screen.getByText('Less than 1 hour left')).toBeInTheDocument();
expect(screen.getByText(/You will lose all access to this course.*?on/s).textContent).toMatch('You will lose all access to this course, including any progress, on April 13.');
expect(screen.getByText(/Upgrading your course enables you/s).textContent).toMatch('Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the benefits of upgrading.');
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
it('renders FBE expiration within 24 hours properly', async () => {
const expirationDate = new Date(dateNow);
expirationDate.setHours(expirationDate.getHours() + 12);
buildAndRender({
accessExpiration: {
expirationDate: expirationDate.toString(),
},
contentTypeGatingEnabled: true,
});
expect(screen.getByRole('heading', { name: 'Course Access Expiration' })).toBeInTheDocument();
expect(screen.getByText('12 hours left')).toBeInTheDocument();
expect(screen.getByText(/You will lose all access to this course.*?on/s)).toHaveTextContent('You will lose all access to this course, including any progress, on April 13.');
expect(screen.getByText(/Upgrading your course enables you/s).textContent).toMatch('Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the benefits of upgrading.');
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
it('renders FBE expiration within 7 days properly', async () => {
const expirationDate = new Date(dateNow);
expirationDate.setDate(expirationDate.getDate() + 6);
buildAndRender({
accessExpiration: {
expirationDate: expirationDate.toString(),
},
contentTypeGatingEnabled: true,
});
expect(screen.getByRole('heading', { name: 'Course Access Expiration' })).toBeInTheDocument();
expect(screen.getByText('6 days left')).toBeInTheDocument(); // setting the time to 12 will mean that it's slightly less than 12
expect(screen.getByText(/You will lose all access to this course.*?on/s).textContent).toMatch('You will lose all access to this course, including any progress, on April 19.');
expect(screen.getByText(/Upgrading your course enables you/s).textContent).toMatch('Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the benefits of upgrading.');
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
it('renders FBE expiration greater than 7 days properly', async () => {
const expirationDate = new Date(dateNow);
expirationDate.setDate(expirationDate.getDate() + 14);
buildAndRender({
accessExpiration: {
expirationDate: expirationDate.toString(),
},
contentTypeGatingEnabled: true,
});
expect(screen.getByRole('heading', { name: 'Upgrade your course today' })).toBeInTheDocument();
expect(screen.getByText(/Course access will expire/s).textContent).toMatch('Course access will expire April 27');
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é');
expect(screen.getByText(/Unlock your access/s).textContent).toMatch('Unlock your access to all course activities, including graded assignments');
expect(screen.getByText(/to course content and materials/s).textContent).toMatch('Full access to course content and materials, even after the course ends');
expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our mission at edX');
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
});
it('renders discount less than an hour properly', async () => {
const accessExpirationDate = new Date(dateNow);
accessExpirationDate.setDate(accessExpirationDate.getDate() + 21);
const discountExpirationDate = new Date(dateNow);
discountExpirationDate.setMinutes(discountExpirationDate.getMinutes() + 30);
buildAndRender({
accessExpiration: {
expirationDate: accessExpirationDate.toString(),
},
contentTypeGatingEnabled: true,
offer: {
expirationDate: discountExpirationDate.toString(),
percentage: 15,
code: 'Welcome15',
discountedPrice: '$126.65',
originalPrice: '$149',
upgradeUrl: 'www.exampleUpgradeUrl.com',
},
});
expect(screen.getByRole('heading', { name: '15% First-Time Learner Discount' })).toBeInTheDocument();
expect(screen.getByText('Less than 1 hour left')).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é');
expect(screen.getByText(/Unlock your access/s).textContent).toMatch('Unlock your access to all course activities, including graded assignments');
expect(screen.getByText(/to course content and materials/s).textContent).toMatch('Full access to course content and materials, even after the course ends');
expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our mission at edX');
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 discount less than a day properly', async () => {
const accessExpirationDate = new Date(dateNow);
accessExpirationDate.setDate(accessExpirationDate.getDate() + 21);
const discountExpirationDate = new Date(dateNow);
discountExpirationDate.setHours(discountExpirationDate.getHours() + 12);
buildAndRender({
accessExpiration: {
expirationDate: accessExpirationDate.toString(),
},
contentTypeGatingEnabled: true,
offer: {
expirationDate: discountExpirationDate.toString(),
percentage: 15,
code: 'Welcome15',
discountedPrice: '$126.65',
originalPrice: '$149',
upgradeUrl: 'www.exampleUpgradeUrl.com',
},
});
expect(screen.getByRole('heading', { name: '15% First-Time Learner Discount' })).toBeInTheDocument();
expect(screen.getByText(/hours left/s).textContent).toMatch('12 hours left');
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é');
expect(screen.getByText(/Unlock your access/s).textContent).toMatch('Unlock your access to all course activities, including graded assignments');
expect(screen.getByText(/to course content and materials/s).textContent).toMatch('Full access to course content and materials, even after the course ends');
expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our mission at edX');
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 discount less a week properly', async () => {
const accessExpirationDate = new Date(dateNow);
accessExpirationDate.setDate(accessExpirationDate.getDate() + 21);
const discountExpirationDate = new Date(dateNow);
discountExpirationDate.setDate(discountExpirationDate.getDate() + 6);
buildAndRender({
accessExpiration: {
expirationDate: accessExpirationDate.toString(),
},
contentTypeGatingEnabled: true,
offer: {
expirationDate: discountExpirationDate.toString(),
percentage: 15,
code: 'Welcome15',
discountedPrice: '$126.65',
originalPrice: '$149',
upgradeUrl: 'www.exampleUpgradeUrl.com',
},
});
expect(screen.getByRole('heading', { name: '15% First-Time Learner Discount' })).toBeInTheDocument();
expect(screen.getByText(/days left/s).textContent).toMatch('6 days left');
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é');
expect(screen.getByText(/Unlock your access/s).textContent).toMatch('Unlock your access to all course activities, including graded assignments');
expect(screen.getByText(/to course content and materials/s).textContent).toMatch('Full access to course content and materials, even after the course ends');
expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our mission at edX');
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 discount less a week access expiration less than a week properly', async () => {
const accessExpirationDate = new Date(dateNow);
accessExpirationDate.setDate(accessExpirationDate.getDate() + 5);
const discountExpirationDate = new Date(dateNow);
discountExpirationDate.setDate(discountExpirationDate.getDate() + 6);
buildAndRender({
accessExpiration: {
expirationDate: accessExpirationDate.toString(),
},
contentTypeGatingEnabled: true,
offer: {
expirationDate: discountExpirationDate.toString(),
percentage: 15,
code: 'Welcome15',
discountedPrice: '$126.65',
originalPrice: '$149',
upgradeUrl: 'www.exampleUpgradeUrl.com',
},
});
expect(screen.getByRole('heading', { name: 'Course Access Expiration' })).toBeInTheDocument();
expect(screen.getByText('5 days left')).toBeInTheDocument(); // setting the time to 12 will mean that it's slightly less than 12
expect(screen.getByText(/You will lose all access to this course.*?on/s).textContent).toMatch('You will lose all access to this course, including any progress, on April 18.');
expect(screen.getByText(/Upgrading your course enables you/s).textContent).toMatch('Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the benefits of upgrading.');
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',
});
});
});

View File

@@ -433,13 +433,10 @@
// Import component-specific sass files
@import "courseware/course/celebration/CelebrationModal.scss";
@import "courseware/course/sidebar/sidebars/notifications/NotificationIcon.scss";
@import "courseware/course/sequence/lock-paywall/LockPaywall.scss";
@import "shared/streak-celebration/StreakCelebrationModal.scss";
@import "courseware/course/content-tools/calculator/calculator.scss";
@import "courseware/course/content-tools/contentTools.scss";
@import "course-home/dates-tab/timeline/Day.scss";
@import "generic/upgrade-notification/UpgradeNotification.scss";
@import "generic/upsell-bullets/UpsellBullets.scss";
@import "course-home/outline-tab/widgets/ProctoringInfoPanel.scss";
@import "course-home/outline-tab/widgets/FlagButton.scss";
@import "course-home/progress-tab/course-completion/CompletionDonutChart.scss";