feat: removes sidebar upgrade and fbe paywall
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 = () => ({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -9,7 +9,6 @@ import PageLoading from '@src/generic/PageLoading';
|
||||
|
||||
import messages from '../messages';
|
||||
import HonorCode from '../honor-code';
|
||||
import LockPaywall from '../lock-paywall';
|
||||
import * as hooks from './hooks';
|
||||
import { modelKeys } from './constants';
|
||||
|
||||
@@ -34,9 +33,7 @@ const UnitSuspense = ({
|
||||
pluginProps={{
|
||||
courseId,
|
||||
}}
|
||||
>
|
||||
<LockPaywall courseId={courseId} />
|
||||
</PluginSlot>
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
{shouldDisplayHonorCode && (
|
||||
|
||||
@@ -7,7 +7,6 @@ import PageLoading from '@src/generic/PageLoading';
|
||||
|
||||
import messages from '../messages';
|
||||
import HonorCode from '../honor-code';
|
||||
import LockPaywall from '../lock-paywall';
|
||||
import hooks from './hooks';
|
||||
import { modelKeys } from './constants';
|
||||
|
||||
@@ -24,7 +23,6 @@ jest.mock('react', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('../honor-code', () => 'HonorCode');
|
||||
jest.mock('../lock-paywall', () => 'LockPaywall');
|
||||
jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }));
|
||||
jest.mock('@src/generic/PageLoading', () => 'PageLoading');
|
||||
|
||||
@@ -62,31 +60,6 @@ describe('UnitSuspense component', () => {
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('LockPaywall', () => {
|
||||
const testNoPaywall = () => {
|
||||
it('does not display LockPaywall', () => {
|
||||
el = shallow(<UnitSuspense {...props} />);
|
||||
expect(el.instance.findByType(LockPaywall).length).toEqual(0);
|
||||
});
|
||||
};
|
||||
describe('gating not enabled', () => { testNoPaywall(); });
|
||||
describe('gating enabled, but no gated content included', () => {
|
||||
beforeEach(() => { mockModels(true, false); });
|
||||
testNoPaywall();
|
||||
});
|
||||
describe('gating enabled, gated content included', () => {
|
||||
beforeEach(() => { mockModels(true, true); });
|
||||
it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => {
|
||||
el = shallow(<UnitSuspense {...props} />);
|
||||
const [component] = el.instance.findByType(LockPaywall);
|
||||
expect(component.parent.type).toEqual('PluginSlot');
|
||||
expect(component.parent.parent.type).toEqual('Suspense');
|
||||
expect(component.parent.parent.props.fallback)
|
||||
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />);
|
||||
expect(component.props.courseId).toEqual(props.courseId);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('HonorCode', () => {
|
||||
it('does not display HonorCode if useShouldDisplayHonorCode => false', () => {
|
||||
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
|
||||
|
||||
@@ -28,7 +28,6 @@ jest.mock('../../bookmark/BookmarkButton', () => 'BookmarkButton');
|
||||
jest.mock('./ContentIFrame', () => 'ContentIFrame');
|
||||
jest.mock('./UnitSuspense', () => 'UnitSuspense');
|
||||
jest.mock('../honor-code', () => 'HonorCode');
|
||||
jest.mock('../lock-paywall', () => 'LockPaywall');
|
||||
|
||||
jest.mock('@src/generic/model-store', () => ({
|
||||
useModel: jest.fn(),
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert, Hyperlink, breakpoints, useWindowSize,
|
||||
} from '@openedx/paragon';
|
||||
import { Locked } from '@openedx/paragon/icons';
|
||||
import SidebarContext from '../../sidebar/SidebarContext';
|
||||
import messages from './messages';
|
||||
import certificateLocked from '../../../../generic/assets/edX_locked_certificate.png';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import { UpgradeButton } from '../../../../generic/upgrade-button';
|
||||
import {
|
||||
VerifiedCertBullet,
|
||||
UnlockGradedBullet,
|
||||
FullAccessBullet,
|
||||
SupportMissionBullet,
|
||||
} from '../../../../generic/upsell-bullets/UpsellBullets';
|
||||
|
||||
const LockPaywall = ({
|
||||
intl,
|
||||
courseId,
|
||||
}) => {
|
||||
const { notificationTrayVisible } = useContext(SidebarContext);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const {
|
||||
accessExpiration,
|
||||
marketingUrl,
|
||||
offer,
|
||||
} = course;
|
||||
|
||||
const {
|
||||
org, verifiedMode,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
// the following variables are set and used for resposive layout to work with
|
||||
// whether the NotificationTray is open or not and if there's an offer with longer text
|
||||
const shouldDisplayBulletPointsBelowCertificate = useWindowSize().width <= breakpoints.large.minWidth;
|
||||
const shouldDisplayGatedContentOneColumn = useWindowSize().width <= breakpoints.extraLarge.minWidth
|
||||
&& notificationTrayVisible;
|
||||
const shouldDisplayGatedContentTwoColumns = useWindowSize().width < breakpoints.large.minWidth
|
||||
&& notificationTrayVisible;
|
||||
const shouldDisplayGatedContentTwoColumnsHalf = useWindowSize().width <= breakpoints.large.minWidth
|
||||
&& !notificationTrayVisible;
|
||||
const shouldWrapTextOnButton = useWindowSize().width > breakpoints.extraSmall.minWidth;
|
||||
|
||||
const accessExpirationDate = accessExpiration ? new Date(accessExpiration.expirationDate) : null;
|
||||
const pastExpirationDeadline = accessExpiration ? new Date(Date.now()) > accessExpirationDate : false;
|
||||
|
||||
if (!verifiedMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const eventProperties = {
|
||||
org_key: org,
|
||||
courserun_key: courseId,
|
||||
};
|
||||
|
||||
const logClick = () => {
|
||||
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
|
||||
...eventProperties,
|
||||
linkCategory: '(none)',
|
||||
linkName: 'in_course_upgrade',
|
||||
linkType: 'link',
|
||||
pageName: 'in_course',
|
||||
});
|
||||
};
|
||||
|
||||
const logClickPastExpiration = () => {
|
||||
sendTrackEvent('edx.bi.ecommerce.gated_content.past_expiration.link_clicked', {
|
||||
...eventProperties,
|
||||
linkCategory: 'gated_content',
|
||||
linkName: 'course_details',
|
||||
linkType: 'link',
|
||||
pageName: 'in_course',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert variant="light" aria-live="off" icon={Locked} className="lock-paywall-container" data-testId="lock-paywall-test-id">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<h4 aria-level="3">
|
||||
<span>{intl.formatMessage(messages['learn.lockPaywall.title'])}</span>
|
||||
</h4>
|
||||
{pastExpirationDeadline ? (
|
||||
<div className="mb-2 upgrade-intro">
|
||||
{intl.formatMessage(messages['learn.lockPaywall.content.pastExpiration'])}
|
||||
<Hyperlink destination={marketingUrl} onClick={logClickPastExpiration} target="_blank">{intl.formatMessage(messages['learn.lockPaywall.courseDetails'])}</Hyperlink>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-2 upgrade-intro">
|
||||
{intl.formatMessage(messages['learn.lockPaywall.content'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={classNames('d-inline-flex flex-row', { 'flex-wrap': notificationTrayVisible || shouldDisplayBulletPointsBelowCertificate })}>
|
||||
<div style={{ float: 'left' }} className="mr-3 mb-2">
|
||||
<img
|
||||
alt={intl.formatMessage(messages['learn.lockPaywall.example.alt'])}
|
||||
src={certificateLocked}
|
||||
className="border-0 certificate-image-banner"
|
||||
style={{ height: '128px', width: '175px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mw-xs list-div">
|
||||
<div className="mb-2">
|
||||
{intl.formatMessage(messages['learn.lockPaywall.list.intro'])}
|
||||
</div>
|
||||
<ul className="fa-ul ml-4 pl-2">
|
||||
<VerifiedCertBullet />
|
||||
<UnlockGradedBullet />
|
||||
<FullAccessBullet />
|
||||
<SupportMissionBullet />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pastExpirationDeadline
|
||||
? null
|
||||
: (
|
||||
<div
|
||||
className={
|
||||
classNames('d-md-flex align-items-md-center text-right', {
|
||||
'col-md-5 mx-md-0': notificationTrayVisible, 'col-md-4 mx-md-3 justify-content-center': !notificationTrayVisible && !shouldDisplayGatedContentTwoColumnsHalf, 'col-md-11 justify-content-end': shouldDisplayGatedContentOneColumn && !shouldDisplayGatedContentTwoColumns, 'col-md-6 justify-content-center': shouldDisplayGatedContentTwoColumnsHalf,
|
||||
})
|
||||
}
|
||||
>
|
||||
<UpgradeButton
|
||||
offer={offer}
|
||||
onClick={logClick}
|
||||
verifiedMode={verifiedMode}
|
||||
style={{ whiteSpace: shouldWrapTextOnButton ? 'nowrap' : null }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
LockPaywall.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default injectIntl(LockPaywall);
|
||||
@@ -1,14 +0,0 @@
|
||||
.alert-content.lock-paywall-container {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lock-paywall-container svg {
|
||||
color: $primary-700;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) and (max-width: 1100px) {
|
||||
.list-div {
|
||||
width: 62%;
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
|
||||
import {
|
||||
fireEvent, initializeTestStore, render, screen,
|
||||
} from '../../../../setupTest';
|
||||
import LockPaywall from './LockPaywall';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
describe('Lock Paywall', () => {
|
||||
let store;
|
||||
const mockData = { notificationTrayVisible: false };
|
||||
|
||||
beforeAll(async () => {
|
||||
store = await initializeTestStore();
|
||||
const { courseware } = store.getState();
|
||||
Object.assign(mockData, {
|
||||
courseId: courseware.courseId,
|
||||
});
|
||||
});
|
||||
|
||||
it('displays unlock link with price', () => {
|
||||
const {
|
||||
currencySymbol,
|
||||
price,
|
||||
upgradeUrl,
|
||||
} = store.getState().models.courseHomeMeta[mockData.courseId].verifiedMode;
|
||||
render(<LockPaywall {...mockData} />);
|
||||
|
||||
const upgradeLink = screen.getByRole('link', { name: `Upgrade for ${currencySymbol}${price}` });
|
||||
expect(upgradeLink).toHaveAttribute('href', `${upgradeUrl}`);
|
||||
});
|
||||
|
||||
it('displays discounted price if there is an offer/first time purchase', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
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 }, false);
|
||||
render(<LockPaywall {...mockData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
|
||||
expect(screen.getByText(/Upgrade for/).textContent).toMatch('$85 ($100)');
|
||||
});
|
||||
|
||||
it('sends analytics event onClick of unlock link', () => {
|
||||
sendTrackEvent.mockClear();
|
||||
|
||||
const {
|
||||
currencySymbol,
|
||||
price,
|
||||
} = store.getState().models.courseHomeMeta[mockData.courseId].verifiedMode;
|
||||
render(<LockPaywall {...mockData} />);
|
||||
|
||||
const upgradeLink = screen.getByRole('link', { name: `Upgrade for ${currencySymbol}${price}` });
|
||||
fireEvent.click(upgradeLink);
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: mockData.courseId,
|
||||
linkCategory: '(none)',
|
||||
linkName: 'in_course_upgrade',
|
||||
linkType: 'link',
|
||||
pageName: 'in_course',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display anything if course does not have verified mode', async () => {
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata', { verified_mode: null });
|
||||
const testStore = await initializeTestStore({ courseHomeMetadata, excludeFetchSequence: true }, false);
|
||||
render(<LockPaywall {...mockData} courseId={courseHomeMetadata.id} />, { store: testStore });
|
||||
|
||||
expect(screen.queryByTestId('lock-paywall-test-id')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays past expiration message if expiration date has expired', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
access_expiration: {
|
||||
expiration_date: '1995-02-22T05:00:00Z',
|
||||
},
|
||||
marketing_url: 'https://example.com/course-details',
|
||||
});
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
render(<LockPaywall {...mockData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
expect(screen.getByText('The upgrade deadline for this course passed. To upgrade, enroll in the next available session.')).toBeInTheDocument();
|
||||
expect(screen.getByText('View Course Details'))
|
||||
.toHaveAttribute('href', 'https://example.com/course-details');
|
||||
});
|
||||
|
||||
it('sends analytics event onClick of past expiration course details link', async () => {
|
||||
sendTrackEvent.mockClear();
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
access_expiration: {
|
||||
expiration_date: '1995-02-22T05:00:00Z',
|
||||
},
|
||||
marketing_url: 'https://example.com/course-details',
|
||||
});
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
render(<LockPaywall {...mockData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
const courseDetailsLink = await screen.getByText('View Course Details');
|
||||
fireEvent.click(courseDetailsLink);
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.gated_content.past_expiration.link_clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: mockData.courseId,
|
||||
linkCategory: 'gated_content',
|
||||
linkName: 'course_details',
|
||||
linkType: 'link',
|
||||
pageName: 'in_course',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './LockPaywall';
|
||||
@@ -1,36 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'learn.lockPaywall.title': {
|
||||
id: 'learn.lockPaywall.title',
|
||||
defaultMessage: 'Graded assignments are locked',
|
||||
description: 'Heading for message shown to indicate that a piece of content is unavailable to audit track users.',
|
||||
},
|
||||
'learn.lockPaywall.content': {
|
||||
id: 'learn.lockPaywall.content',
|
||||
defaultMessage: 'Upgrade to gain access to locked features like this one and get the most out of your course.',
|
||||
description: 'Message shown to indicate that a piece of content is unavailable to audit track users.',
|
||||
},
|
||||
'learn.lockPaywall.content.pastExpiration': {
|
||||
id: 'learn.lockPaywall.content.pastExpiration',
|
||||
defaultMessage: 'The upgrade deadline for this course passed. To upgrade, enroll in the next available session. ',
|
||||
description: 'Message shown to indicate that a piece of content is unavailable to audit track users in a course where the expiration deadline has passed.',
|
||||
},
|
||||
'learn.lockPaywall.courseDetails': {
|
||||
id: 'learn.lockPaywall.courseDetails',
|
||||
defaultMessage: 'View Course Details',
|
||||
description: 'Link to the course details page for this course with a past expiration date.',
|
||||
},
|
||||
'learn.lockPaywall.example.alt': {
|
||||
id: 'learn.lockPaywall.example.alt',
|
||||
defaultMessage: 'Example Certificate',
|
||||
description: 'Alternate text displayed when the example certificate image cannot be displayed.',
|
||||
},
|
||||
'learn.lockPaywall.list.intro': {
|
||||
id: 'learn.lockPaywall.list.intro',
|
||||
defaultMessage: 'When you upgrade, you:',
|
||||
description: 'Text displayed to introduce the list of benefits from upgrading.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import React from 'react';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
const CheckmarkBullet = () => (
|
||||
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||
);
|
||||
|
||||
// Must be child of a <ul className="fa-ul">
|
||||
export const VerifiedCertBullet = () => {
|
||||
const verifiedCertLink = (
|
||||
<a className="inline-link-underline font-weight-bold" rel="noopener noreferrer" target="_blank" href={`${getConfig().MARKETING_SITE_BASE_URL}/verified-certificate`}>
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.verifiedCertBullet.verifiedCert"
|
||||
defaultMessage="verified certificate"
|
||||
description="Bolded words 'verified certificate', which is the name of credential the learner receives."
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<li className="upsell-bullet">
|
||||
<CheckmarkBullet />
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.verifiedCertBullet"
|
||||
defaultMessage="Earn a {verifiedCertLink} of completion to showcase on your resumé"
|
||||
description="Bullet showcasing benefit of earned credential."
|
||||
values={{ verifiedCertLink }}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
// Must be child of a <ul className="fa-ul">
|
||||
export const UnlockGradedBullet = () => {
|
||||
const gradedAssignmentsInBoldText = (
|
||||
<span className="font-weight-bold">
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.unlockGradedBullet.gradedAssignments"
|
||||
defaultMessage="graded assignments"
|
||||
description="Bolded words 'graded assignments', which are the bolded portion of a bullet point highlighting that course content is unlocked when purchasing an upgrade. Graded assignments are any course content that is graded and are unlocked by upgrading to verified certificates."
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<li className="upsell-bullet">
|
||||
<CheckmarkBullet />
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.unlockGradedBullet"
|
||||
defaultMessage="Unlock your access to all course activities, including {gradedAssignmentsInBoldText}"
|
||||
description="Bullet showcasing benefit of additional course material."
|
||||
values={{ gradedAssignmentsInBoldText }}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
// Must be child of a <ul className="fa-ul">
|
||||
export const FullAccessBullet = () => {
|
||||
const fullAccessInBoldText = (
|
||||
<span className="font-weight-bold">
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.fullAccessBullet.fullAccess"
|
||||
defaultMessage="Full access"
|
||||
description="Bolded phrase 'Full access', which is the bolded portion of a bullet point highlighting that access to course content will not have time limits."
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<li className="upsell-bullet">
|
||||
<CheckmarkBullet />
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.fullAccessBullet"
|
||||
defaultMessage="{fullAccessInBoldText} to course content and materials, even after the course ends"
|
||||
description="Bullet showcasing upgrade lifts access durations."
|
||||
values={{ fullAccessInBoldText }}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
// Must be child of a <ul className="fa-ul">
|
||||
export const SupportMissionBullet = () => {
|
||||
const missionInBoldText = (
|
||||
<span className="font-weight-bold">
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.supportMissionBullet.mission"
|
||||
defaultMessage="mission"
|
||||
description="Bolded word 'mission', which is the bolded portion of a bullet point encouraging the learner to support the goals of the website."
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<li className="upsell-bullet">
|
||||
<CheckmarkBullet />
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.supportMissionBullet"
|
||||
defaultMessage="Support our {missionInBoldText} at {siteName}"
|
||||
description="Bullet encouraging user to support edX's goals."
|
||||
values={{ missionInBoldText, siteName: getConfig().SITE_NAME }}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
.upsell-bullet > .fa-li {
|
||||
left: -31px;
|
||||
padding-right: 22px;
|
||||
}
|
||||
|
||||
.inline-link-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.upsell-bullet a {
|
||||
color: $primary-500;
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
initializeMockApp,
|
||||
render,
|
||||
screen,
|
||||
} from '../../setupTest';
|
||||
|
||||
import {
|
||||
VerifiedCertBullet,
|
||||
UnlockGradedBullet,
|
||||
FullAccessBullet,
|
||||
SupportMissionBullet,
|
||||
} from './UpsellBullets';
|
||||
|
||||
initializeMockApp();
|
||||
|
||||
describe('UpsellBullets', () => {
|
||||
const bullets = (
|
||||
<>
|
||||
<VerifiedCertBullet />
|
||||
<UnlockGradedBullet />
|
||||
<FullAccessBullet />
|
||||
<SupportMissionBullet />
|
||||
</>
|
||||
);
|
||||
|
||||
it('upsell bullet text properly rendered', async () => {
|
||||
render(bullets);
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user