-
Notifications
You must be signed in to change notification settings - Fork 3
feat: adding accesslock plugin to MFE #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import React, { useCallback } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { useNavigate } from 'react-router-dom'; | ||
| import { faCheck } from '@fortawesome/free-solid-svg-icons'; | ||
|
Check failure on line 4 in src/courseware/course/sequence/AccessLock/AccessLock.jsx
|
||
| import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||
| import { faLock } from '@fortawesome/free-solid-svg-icons'; | ||
| import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; | ||
| import CompleteIcon from '../sequence-navigation/CompleteIcon'; | ||
| import { Button } from '@openedx/paragon'; | ||
| import messages from './messages'; | ||
|
|
||
| const AccessLock = ({ | ||
| courseId | ||
|
Check failure on line 13 in src/courseware/course/sequence/AccessLock/AccessLock.jsx
|
||
| }) => { | ||
| const intl = useIntl(); | ||
| const handleClick = () => {}; | ||
|
|
||
| const Check = <CompleteIcon size="md" className="mr-2" />; | ||
| const DisplayPrice = () => { | ||
|
Check failure on line 19 in src/courseware/course/sequence/AccessLock/AccessLock.jsx
|
||
| return (<> | ||
| <span>{intl.formatMessage(messages['learn.accessLock.upgrade.buttonText'])}</span> | ||
| <span className='large-weight'>$118.15 </span> | ||
| <span style= {{ textDecoration: 'line-through' }}>($139)</span> | ||
| </>) | ||
| }; | ||
| return ( | ||
| <> | ||
| <h3> | ||
| <FontAwesomeIcon icon={faLock} /> | ||
| {intl.formatMessage(messages['learn.accessLock.content.locked'])} | ||
| </h3> | ||
| <p> | ||
| {intl.formatMessage(messages['learn.accessLock.upgrade'])} | ||
| </p> | ||
| <div className="d-flex flex-grow-1 justify-between items-start"> | ||
| <div> | ||
| <p> | ||
| {intl.formatMessage(messages['learn.accessLock.upgrade.header'])} | ||
| </p> | ||
| <ul className="list-unstyled mr-4"> | ||
| <li>{Check}{intl.formatMessage(messages['learn.accessLock.upgrade.benefitOne'])}</li> | ||
| <li>{Check}{intl.formatMessage(messages['learn.accessLock.upgrade.benefitTwo'])}</li> | ||
| <li>{Check}{intl.formatMessage(messages['learn.accessLock.upgrade.benefitThree'])}</li> | ||
| <li>{Check}{intl.formatMessage(messages['learn.accessLock.upgrade.benefitFour'])}</li> | ||
| </ul> | ||
| </div> | ||
| <div className='d-flex items-right pl-6'> | ||
| <p> | ||
| <Button variant="primary" onClick={handleClick}> <DisplayPrice /></Button> | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </> | ||
| ); | ||
| }; | ||
| AccessLock.propTypes = { | ||
| courseId: PropTypes.string.isRequired, | ||
| }; | ||
| export default AccessLock; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import React from 'react'; | ||
| import { | ||
| render, screen, fireEvent, initializeMockApp, | ||
| } from '../../../../setupTest'; | ||
| import AssignmentLock from './AccessLock'; | ||
|
|
||
| const mockNavigate = jest.fn(); | ||
|
|
||
| jest.mock('react-router-dom', () => ({ | ||
| ...jest.requireActual('react-router-dom'), | ||
| useNavigate: () => mockNavigate, | ||
| })); | ||
|
|
||
| describe('Assignment Lock', () => { | ||
| const mockData = { | ||
| courseId: 'test-course-id', | ||
| prereqSectionName: 'test-prerequisite-section-name', | ||
| prereqId: 'test-prerequisite-id', | ||
| sequenceTitle: 'test-sequence-title', | ||
| }; | ||
|
|
||
| beforeAll(async () => { | ||
| await initializeMockApp(); | ||
| }); | ||
|
|
||
| it('displays sequence title along with lock icon', () => { | ||
| const { container } = render(<AssignmentLock {...mockData} />, { wrapWithRouter: true }); | ||
|
|
||
| const lockIcon = container.querySelector('svg'); | ||
| expect(lockIcon).toHaveClass('fa-lock'); | ||
| expect(lockIcon.parentElement).toHaveTextContent(mockData.sequenceTitle); | ||
| }); | ||
|
|
||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from './AccessLock'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { defineMessages } from '@edx/frontend-platform/i18n'; | ||
|
|
||
| const messages = defineMessages({ | ||
| 'learn.accessLock.content.locked': { | ||
| id: 'learn.accessLock.content.locked', | ||
| defaultMessage: 'The content you are trying to access is locked', | ||
| description: 'Message shown to indicate that a piece of content is unavailable and has a prerequisite.', | ||
| }, | ||
| 'learn.accessLock.upgrade': { | ||
| id: 'learn.accessLock.upgrade', | ||
| defaultMessage: "Upgrade to gain access to locked features like this one and get the most out of your course.", | ||
| description: '', | ||
| }, | ||
| 'learn.accessLock.upgrade.header': { | ||
| id: 'learn.accessLock.upgrade.header', | ||
| defaultMessage: 'When you upgrade, you get:', | ||
| description: '', | ||
| }, | ||
| 'learn.accessLock.upgrade.benefitOne': { | ||
| id: 'learn.accessLock.upgrade.benefitOne', | ||
| defaultMessage: 'Feedback and graded assignments', | ||
| description: '', | ||
| }, | ||
| 'learn.accessLock.upgrade.benefitTwo': { | ||
| id: 'learn.accessLock.upgrade.benefitTwo', | ||
| defaultMessage: 'Shareable certificate upon completion', | ||
| description: '', | ||
| }, | ||
| 'learn.accessLock.upgrade.benefitThree': { | ||
| id: 'learn.accessLock.upgrade.benefitThree', | ||
| defaultMessage: 'Lifetime access to course materials', | ||
| description: '', | ||
| }, | ||
| 'learn.accessLock.upgrade.benefitFour': { | ||
| id: 'learn.accessLock.upgrade.benefitFour', | ||
| defaultMessage: 'Instructor support', | ||
| description: '', | ||
| }, | ||
| 'learn.accessLock.upgrade.buttonText': { | ||
| id: 'learn.accessLock.upgrade.buttonText', | ||
| defaultMessage: 'Upgrade for ', | ||
| description: '', | ||
| }, | ||
| }); | ||
|
|
||
| export default messages; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; | |
| import { useModel } from '@src/generic/model-store'; | ||
| import PageLoading from '@src/generic/PageLoading'; | ||
| import { GatedUnitContentMessageSlot } from '../../../../plugin-slots/GatedUnitContentMessageSlot'; | ||
| import { AccessLockContentMessageSlot } from '../../../../plugin-slots/AccessLockContentMessageSlot'; | ||
|
|
||
| import messages from '../messages'; | ||
| import HonorCode from '../honor-code'; | ||
|
|
@@ -26,9 +27,9 @@ const UnitSuspense = ({ | |
|
|
||
| return ( | ||
| <> | ||
| {shouldDisplayContentGating && ( | ||
| {(shouldDisplayContentGating || unit.accessRestricted) && ( | ||
| <Suspense fallback={<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />}> | ||
| <GatedUnitContentMessageSlot courseId={courseId} /> | ||
| {unit.accessRestricted ? <AccessLockContentMessageSlot courseId={courseId} /> : <GatedUnitContentMessageSlot courseId={courseId} />} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you help me understand why you're adding a new plugin slot that, functionally, is almost identical to |
||
| </Suspense> | ||
| )} | ||
| {shouldDisplayHonorCode && ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # Gated Unit Content Message Slot | ||
|
|
||
| ### Slot ID: `org.openedx.frontend.learning.access_lock_content_message.v1` | ||
|
|
||
| ### Slot ID Aliases | ||
| * `access_lock_content_message_slot` | ||
|
|
||
| ### Props: | ||
| * `courseId` - String identifier for the current course | ||
|
|
||
| ## Description | ||
|
|
||
| This slot is used to customize the message displayed when course content is gated or locked for learners who haven't upgraded to a verified track. It appears when a unit contains content that requires a paid enrollment (such as graded assignments) and the learner is on the audit track. | ||
|
|
||
| The default implementation shows a `LockPaywall` component that displays an upgrade message with benefits of upgrading, including access to graded assignments, certificates, and full course features. | ||
|
|
||
| This slot is conditionally rendered only when `contentTypeGatingEnabled` is true and the unit `containsContentTypeGatedContent`. | ||
|
|
||
| ## Example | ||
|
|
||
| The following `env.config.jsx` will replace the default paywall message with a custom gated content component. | ||
|
|
||
|  | ||
|
|
||
| ```js | ||
| import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; | ||
|
|
||
| const config = { | ||
| pluginSlots: { | ||
| 'org.openedx.frontend.learning.access_lock_content_message.v1': { | ||
| plugins: [ | ||
| { | ||
| op: PLUGIN_OPERATIONS.Insert, | ||
| widgetId: 'default_contents', | ||
| widget: { | ||
| id: 'custom_gated_message', | ||
| type: DIRECT_PLUGIN, | ||
| RenderWidget: ({ courseId }) => ( | ||
| <div className="alert alert-warning" role="alert"> | ||
| <div className="d-flex align-items-center mb-3"> | ||
| <i className="fa fa-lock fa-2x me-3" aria-hidden="true"></i> | ||
| <div> | ||
| <h4 className="alert-heading mb-1">Premium Content</h4> | ||
| <p className="mb-0">This content is available to verified learners only.</p> | ||
| </div> | ||
| </div> | ||
| <hr /> | ||
| <p className="mb-3"> | ||
| Upgrade your enrollment for course {courseId} to access: | ||
| </p> | ||
| <ul className="mb-3"> | ||
| <li>✅ Graded assignments and quizzes</li> | ||
| <li>🏆 Verified certificate upon completion</li> | ||
| <li>💬 Full discussion forum access</li> | ||
| <li>📱 Mobile app offline access</li> | ||
| </ul> | ||
| <button className="btn btn-success"> | ||
| Upgrade Now | ||
| </button> | ||
| </div> | ||
| ), | ||
| }, | ||
| }, | ||
| ] | ||
| } | ||
| }, | ||
| } | ||
|
|
||
| export default config; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import React from 'react'; | ||
|
|
||
| import { PluginSlot } from '@openedx/frontend-plugin-framework'; | ||
| import AccessLock from '../../courseware/course/sequence/AccessLock'; | ||
|
|
||
|
|
||
| export const AccessLockContentMessageSlot = ({ | ||
| courseId, | ||
| } : AccessLockContentMessageSlotProps) => ( | ||
| <PluginSlot | ||
| id="org.openedx.frontend.learning.access_lock_content_message.v1" | ||
| idAliases={['access_lock_content_message_slot']} | ||
| pluginProps={{ | ||
| courseId, | ||
| }} | ||
| > | ||
| <AccessLock courseId={courseId} /> | ||
| </PluginSlot> | ||
| ); | ||
|
|
||
| interface AccessLockContentMessageSlotProps { | ||
| courseId: string; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a duplicate of the
AccessLockin https://github.com/edx/frontend-plugin-advertisements/pull/106. Is there a reason this shouldn't be removed?