-
Notifications
You must be signed in to change notification settings - Fork 203
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c846a43
commit db114cb
Showing
6 changed files
with
325 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
src/courseware/course/sequence/lock-paywall/LockPaywall.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
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'; | ||
Check failure on line 20 in src/courseware/course/sequence/lock-paywall/LockPaywall.jsx GitHub Actions / tests
|
||
|
||
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); |
14 changes: 14 additions & 0 deletions
14
src/courseware/course/sequence/lock-paywall/LockPaywall.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
.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%; | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
src/courseware/course/sequence/lock-paywall/LockPaywall.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
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', | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './LockPaywall'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
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; |