diff --git a/package-lock.json b/package-lock.json index 0e1fa2ee..cf44f4b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1413,9 +1413,9 @@ } }, "@edx/paragon": { - "version": "13.13.5", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-13.13.5.tgz", - "integrity": "sha512-6q60Lj5dbzZbcXfNrpHXqn4tKQYf1KmcISOe//un0JTSQr6/LnZjHZoZfmzDaRX/2KEDaLCTqCefvKTpB26qCQ==", + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-13.16.0.tgz", + "integrity": "sha512-E1XCpiHoD0TaTUV6o5FxfkxfUhtBmSsUCmR7LSTPXpiuI7ouK2PxbRoxCT8CnHbSAeeYKN0vPHNGEd9hFRm7zg==", "requires": { "@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/free-solid-svg-icons": "^5.14.0", @@ -2307,9 +2307,9 @@ } }, "@popperjs/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.0.tgz", - "integrity": "sha512-wjtKehFAIARq2OxK8j3JrggNlEslJfNuSm2ArteIbKyRMts2g0a7KzTxfRVNUM+O0gnBJ2hNV8nWPOYBgI1sew==" + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.1.tgz", + "integrity": "sha512-DvJbbn3dUgMxDnJLH+RZQPnXak1h4ZVYQ7CWiFWjQwBFkVajT4rfw2PdpHLTSTwxrYfnoEXkuBiwkDm6tPMQeA==" }, "@reduxjs/toolkit": { "version": "1.3.6", @@ -3016,11 +3016,12 @@ "dev": true }, "@types/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.2.tgz", - "integrity": "sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", + "integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==", "requires": { "@types/prop-types": "*", + "@types/scheduler": "*", "csstype": "^3.0.2" } }, @@ -3032,6 +3033,11 @@ "@types/react": "*" } }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" + }, "@types/schema-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/schema-utils/-/schema-utils-2.4.0.tgz", diff --git a/package.json b/package.json index f30783e8..bc8ef5c8 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@edx/frontend-component-footer": "10.1.4", "@edx/frontend-enterprise": "4.2.3", "@edx/frontend-platform": "1.8.4", - "@edx/paragon": "13.13.5", + "@edx/paragon": "13.16.0", "@fortawesome/fontawesome-svg-core": "1.2.34", "@fortawesome/free-brands-svg-icons": "5.13.1", "@fortawesome/free-regular-svg-icons": "5.13.1", diff --git a/src/course-home/dates-tab/DatesTab.jsx b/src/course-home/dates-tab/DatesTab.jsx index 54efc3f1..7abdd015 100644 --- a/src/course-home/dates-tab/DatesTab.jsx +++ b/src/course-home/dates-tab/DatesTab.jsx @@ -9,6 +9,9 @@ import DatesBannerContainer from '../dates-banner/DatesBannerContainer'; import { fetchDatesTab } from '../data'; import { useModel } from '../../generic/model-store'; +/** [MM-P2P] Experiment */ +import { initDatesMMP2P } from '../../experiments/mm-p2p'; + function DatesTab({ intl }) { const { courseId, @@ -20,19 +23,25 @@ function DatesTab({ intl }) { hasEnded, } = useModel('dates', courseId); + /** [MM-P2P] Experiment */ + const mmp2p = initDatesMMP2P(courseId); + return ( <>
{intl.formatMessage(messages.title)}
- - + { /** [MM-P2P] Experiment */ } + { !mmp2p.state.isEnabled && ( + + ) } + ); } diff --git a/src/course-home/dates-tab/Day.jsx b/src/course-home/dates-tab/Day.jsx index c01c4a19..25126341 100644 --- a/src/course-home/dates-tab/Day.jsx +++ b/src/course-home/dates-tab/Day.jsx @@ -13,7 +13,13 @@ import { getBadgeListAndColor } from './badgelist'; import { isLearnerAssignment } from './utils'; function Day({ - date, first, intl, items, last, + date, + first, + intl, + items, + last, + /** [MM-P2P] Example */ + mmp2p, }) { const { courseId, @@ -26,6 +32,11 @@ function Day({ const { color, badges } = getBadgeListAndColor(date, intl, null, items); + /** [MM-P2P] Experiment */ + const mmp2pOverride = ( + mmp2p.state.isEnabled + && items.some((item) => item.dateType === 'verified-upgrade-deadline') + ); return (
  • {/* Top Line */} @@ -42,7 +53,8 @@ function Day({

    {items.map((item) => { - const { badges: itemBadges } = getBadgeListAndColor(date, intl, item, items); + /** [MM-P2P] Experiment (conditional) */ + const { badges: itemBadges } = mmp2pOverride + ? getBadgeListAndColor(new Date(mmp2p.state.upgradeDeadline), intl, item, items) + : getBadgeListAndColor(date, intl, item, items); + const showLink = item.link && isLearnerAssignment(item); const title = showLink ? ({item.title}) : item.title; const available = item.learnerHasAccess && (item.link || !isLearnerAssignment(item)); const textColor = available ? 'text-dark-500' : 'text-dark-200'; + return (

    @@ -76,7 +93,15 @@ function Day({ )}
    - {item.description &&
    {item.description}
    } + { /** [MM-P2P] Experiment (conditional) */ } + { mmp2pOverride + ? ( +
    + You are still elligible to upgrade to a Verified Certificate! +   Unlock full course access and highlight the knowledge you'll gain. +
    + ) + : (item.description &&
    {item.description}
    )}
    ); })} @@ -99,11 +124,25 @@ Day.propTypes = { title: PropTypes.string, })).isRequired, last: PropTypes.bool, + /** [MM-P2P] Experiment */ + mmp2p: PropTypes.shape({ + state: PropTypes.shape({ + isEnabled: PropTypes.bool.isRequired, + upgradeDeadline: PropTypes.string, + }), + }), }; Day.defaultProps = { first: false, last: false, + /** [MM-P2P] Experiment */ + mmp2p: { + state: { + isEnabled: false, + upgradeDeadline: '', + }, + }, }; export default injectIntl(Day); diff --git a/src/course-home/dates-tab/Timeline.jsx b/src/course-home/dates-tab/Timeline.jsx index 44bfc2c3..8a166663 100644 --- a/src/course-home/dates-tab/Timeline.jsx +++ b/src/course-home/dates-tab/Timeline.jsx @@ -1,4 +1,6 @@ import React from 'react'; +/** [MM-P2P] Experiment (import) */ +import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { useModel } from '../../generic/model-store'; @@ -6,7 +8,8 @@ import { useModel } from '../../generic/model-store'; import Day from './Day'; import { daycmp, isLearnerAssignment } from './utils'; -export default function Timeline() { +/** [MM-P2P] Experiment (argument) */ +export default function Timeline({ mmp2p }) { const { courseId, } = useSelector(state => state.courseHome); @@ -63,8 +66,17 @@ export default function Timeline() { return (
      {groupedDates.map((groupedDate) => ( - + ))}
    ); } + +/** [MM-P2P] Experiment */ +Timeline.propTypes = { + mmp2p: PropTypes.shape({}), +}; + +Timeline.defaultProps = { + mmp2p: {}, +}; diff --git a/src/course-home/outline-tab/DateSummary.jsx b/src/course-home/outline-tab/DateSummary.jsx index 43845c5d..a7aadc84 100644 --- a/src/course-home/outline-tab/DateSummary.jsx +++ b/src/course-home/outline-tab/DateSummary.jsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons'; import { FormattedDate } from '@edx/frontend-platform/i18n'; -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { isLearnerAssignment } from '../dates-tab/utils'; import './DateSummary.scss'; @@ -9,22 +9,14 @@ import './DateSummary.scss'; export default function DateSummary({ dateBlock, userTimezone, + /** [MM-P2P] Experiment */ + mmp2p, }) { const linkedTitle = dateBlock.link && isLearnerAssignment(dateBlock); const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {}; - /** [MM-P2P experiment] */ - const [showMMP2P, setShowMMP2P] = useState(window.experiment__home_dates_bShowMMP2P); - if ( - dateBlock.dateType === 'verified-upgrade-deadline' - && window.experiment__home_dates_showMMP2P === undefined - ) { - window.experiment__home_dates_showMMP2P = (value) => { - setShowMMP2P(!!value); - window.experiment__home_dates_bShowMMP2P = !!value; - }; - } - const activateMMP2P = (showMMP2P && dateBlock.dateType === 'verified-upgrade-deadline'); + /** [MM-P2P] Experiment */ + const showMMP2P = mmp2p.state.isEnabled && (dateBlock.dateType === 'verified-upgrade-deadline'); return (
  • @@ -32,7 +24,8 @@ export default function DateSummary({
    - {activateMMP2P ? ( + {/** [MM-P2P] Experiment (conditional) */} + { showMMP2P ? (
    @@ -84,8 +78,22 @@ DateSummary.propTypes = { learnerHasAccess: PropTypes.bool, }).isRequired, userTimezone: PropTypes.string, + /** [MM-P2P] Experiment */ + mmp2p: PropTypes.shape({ + state: PropTypes.shape({ + isEnabled: PropTypes.bool.isRequired, + upgradeDeadline: PropTypes.string, + }), + }), }; DateSummary.defaultProps = { userTimezone: null, + /** [MM-P2P] Experiment */ + mmp2p: { + state: { + isEnabled: false, + upgradeDeadline: '', + }, + }, }; diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 64416731..086a3723 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -28,6 +28,9 @@ import { useModel } from '../../generic/model-store'; import WelcomeMessage from './widgets/WelcomeMessage'; import ProctoringInfoPanel from './widgets/ProctoringInfoPanel'; +/** [MM-P2P] Experiment */ +import { initHomeMMP2P, MMP2PFlyover } from '../../experiments/mm-p2p'; + function OutlineTab({ intl }) { const { courseId, @@ -88,6 +91,9 @@ function OutlineTab({ intl }) { const courseSock = useRef(null); + /** [[MM-P2P] Experiment */ + const MMP2P = initHomeMMP2P(courseId); + return ( <> )}
    -
    + {/** [MM-P2P] Experiment (className for optimizely trigger) */} +
    - + { /** [MM-P2P] Experiment (the conditional) */ } + { !MMP2P.state.isEnabled + && ( + + )} {courseDateBlocks && ( )} {!courseGoalToDisplay && goalOptions && goalOptions.length > 0 && ( @@ -189,12 +202,21 @@ function OutlineTab({ intl }) { - { courseSock.current.showToUser(); } : null} - /> + { /** [MM-P2P] Experiment (conditional) */ } + { MMP2P.state.isEnabled + ? + : ( + { courseSock.current.showToUser(); } : null + } + /> + )} ))} @@ -42,10 +49,14 @@ function CourseDates({ courseId, intl }) { CourseDates.propTypes = { courseId: PropTypes.string, intl: intlShape.isRequired, + /** [MM-P2P] Experiment */ + mmp2p: PropTypes.shape({}), }; CourseDates.defaultProps = { courseId: null, + /** [MM-P2P] Experiment */ + mmp2p: {}, }; export default injectIntl(CourseDates); diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx index 2ebadcf4..afae9884 100644 --- a/src/courseware/course/Course.jsx +++ b/src/courseware/course/Course.jsx @@ -16,6 +16,9 @@ import CourseBreadcrumbs from './CourseBreadcrumbs'; import CourseSock from '../../generic/course-sock'; import { useModel } from '../../generic/model-store'; +/** [MM-P2P] Experiment */ +import { initCoursewareMMP2P, MMP2PBlockModal } from '../../experiments/mm-p2p'; + function Course({ courseId, sequenceId, @@ -83,19 +86,25 @@ function Course({ }; // The above block of code should be reverted after the REV1512 experiment + /** [MM-P2P] Experiment */ + const MMP2P = initCoursewareMMP2P(courseId, sequenceId, unitId); + return ( <> {`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`} - + { /** This conditional is for the [MM-P2P] Experiment */} + { !MMP2P.state.isEnabled && ( + + )} {celebrationOpen && ( )} + { /** [MM-P2P] Experiment */ } + { MMP2P.meta.modalLock && } ); } diff --git a/src/courseware/course/CourseBreadcrumbs.jsx b/src/courseware/course/CourseBreadcrumbs.jsx index b6e7b5a6..1111182f 100644 --- a/src/courseware/course/CourseBreadcrumbs.jsx +++ b/src/courseware/course/CourseBreadcrumbs.jsx @@ -7,6 +7,9 @@ import { faHome } from '@fortawesome/free-solid-svg-icons'; import { useSelector } from 'react-redux'; import { useModel } from '../../generic/model-store'; +/** [MM-P2P] Experiment */ +import { MMP2PFlyoverTrigger } from '../../experiments/mm-p2p'; + function CourseBreadcrumb({ url, children, withSeparator, ...attrs }) { @@ -39,6 +42,8 @@ export default function CourseBreadcrumbs({ toggleREV1512Flyover, /* This line should be reverted after the REV1512 experiment */ REV1512FlyoverEnabled, /* This line should be reverted after the REV1512 experiment */ isREV1512FlyoverVisible, /* This line should be reverted after the REV1512 experiment */ + /** [MM-P2P] Experiment */ + mmp2p, }) { const course = useModel('coursewareMeta', courseId); const sequence = useModel('sequences', sequenceId); @@ -92,20 +97,24 @@ export default function CourseBreadcrumbs({ ))} {/* The below block of code should be reverted after the REV1512 experiment */} - {REV1512FlyoverEnabled - && !isMobile && ( - + {/** [MM-P2P] Experiment (additional conditional) */} + {REV1512FlyoverEnabled && !mmp2p.state.isEnabled && !isMobile && ( + + )} + {/** [MM-P2P] Experiment */} + {mmp2p.state.isEnabled && ( + )} @@ -119,9 +128,19 @@ CourseBreadcrumbs.propTypes = { toggleREV1512Flyover: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */ REV1512FlyoverEnabled: PropTypes.bool.isRequired, /* This line should be reverted after the REV1512 experiment */ isREV1512FlyoverVisible: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */ + + /** [MM-P2P] Experiment */ + mmp2p: PropTypes.shape({ + state: PropTypes.shape({ + isEnabled: PropTypes.bool.isRequired, + }), + }), }; CourseBreadcrumbs.defaultProps = { sectionId: null, sequenceId: null, + + /** [MM-P2P] Experiment */ + mmp2p: {}, }; diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index 75ab27d8..36d21060 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -24,6 +24,9 @@ import messages from './messages'; import { SequenceNavigation, UnitNavigation } from './sequence-navigation'; import SequenceContent from './SequenceContent'; +/** [MM-P2P] Experiment */ +import { MMP2PFlyover, MMP2PFlyoverMobile } from '../../../experiments/mm-p2p'; + function REV1512Flyover({ toggleREV1512Flyover }) { // This component should be reverted after the REV1512 experiment return ( @@ -192,6 +195,8 @@ function Sequence({ isREV1512FlyoverVisible, /* This line should be reverted after the REV1512 experiment */ REV1512FlyoverEnabled, /* This line should be reverted after the REV1512 experiment */ toggleREV1512Flyover, /* This line should be reverted after the REV1512 experiment */ + /** [MM-P2P] Experiment */ + mmp2p, }) { const course = useModel('coursewareMeta', courseId); const sequence = useModel('sequences', sequenceId); @@ -318,6 +323,9 @@ function Sequence({ toggleREV1512Flyover={toggleREV1512Flyover} /* This line should be reverted after REV1512 experiment */ REV1512FlyoverEnabled={REV1512FlyoverEnabled} /* This line should be reverted after REV1512 experiment */ isREV1512FlyoverVisible={isREV1512FlyoverVisible} /* should be reverted after REV1512 experiment */ + /** [MM-P2P] Experiment */ + mmp2p={mmp2p} + nextSequenceHandler={() => { logEvent('edx.ui.lms.sequence.next_selected', 'top'); handleNext(); @@ -339,6 +347,8 @@ function Sequence({ sequenceId={sequenceId} unitId={unitId} unitLoadedHandler={handleUnitLoaded} + /** [MM-P2P] Experiment */ + mmp2p={mmp2p} /> {unitHasLoaded && (
    {/* This block of code should be reverted post REV1512 experiment */} - {REV1512FlyoverEnabled && isREV1512FlyoverVisible() && ( + {/** [MM-P2P] Experiment (additional conditional) */} + {!mmp2p.state.isEnabled && REV1512FlyoverEnabled && isREV1512FlyoverVisible() && ( isMobile ? : )} + {/** [MM-P2P] Experiment */} + {(mmp2p.state.isEnabled && mmp2p.flyover.isVisible) && ( + isMobile + ? + : + )}
    @@ -388,11 +405,31 @@ Sequence.propTypes = { toggleREV1512Flyover: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */ isREV1512FlyoverVisible: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */ REV1512FlyoverEnabled: PropTypes.bool.isRequired, /* This line should be reverted after the REV1512 experiment */ + + /** [MM-P2P] Experiment */ + mmp2p: PropTypes.shape({ + flyover: PropTypes.shape({ + isVisible: PropTypes.bool.isRequired, + }), + meta: PropTypes.shape({ + showLock: PropTypes.bool, + }), + state: PropTypes.shape({ + isEnabled: PropTypes.bool.isRequired, + }), + }), }; Sequence.defaultProps = { sequenceId: null, unitId: null, + + /** [MM-P2P] Experiment */ + mmp2p: { + flyover: { isVisible: false }, + meta: { showLock: false }, + state: { isEnabled: false }, + }, }; export default injectIntl(Sequence); diff --git a/src/courseware/course/sequence/SequenceContent.jsx b/src/courseware/course/sequence/SequenceContent.jsx index 51b796b8..d6eff201 100644 --- a/src/courseware/course/sequence/SequenceContent.jsx +++ b/src/courseware/course/sequence/SequenceContent.jsx @@ -10,7 +10,14 @@ import Unit from './Unit'; const ContentLock = React.lazy(() => import('./content-lock')); function SequenceContent({ - gated, intl, courseId, sequenceId, unitId, unitLoadedHandler, + gated, + intl, + courseId, + sequenceId, + unitId, + unitLoadedHandler, + /** [MM-P2P] Experiment */ + mmp2p, }) { const sequence = useModel('sequences', sequenceId); @@ -54,6 +61,8 @@ function SequenceContent({ key={unitId} id={unitId} onLoaded={unitLoadedHandler} + /** [MM-P2P] Experiment */ + mmp2p={mmp2p} /> ); } @@ -65,10 +74,28 @@ SequenceContent.propTypes = { unitId: PropTypes.string, unitLoadedHandler: PropTypes.func.isRequired, intl: intlShape.isRequired, + /** [MM-P2P] Experiment */ + mmp2p: PropTypes.shape({ + flyover: PropTypes.shape({ + isVisible: PropTypes.bool.isRequired, + }), + meta: PropTypes.shape({ + showLock: PropTypes.bool, + }), + state: PropTypes.shape({ + isEnabled: PropTypes.bool.isRequired, + }), + }), }; SequenceContent.defaultProps = { unitId: null, + /** [MM-P2P] Experiment */ + mmp2p: { + flyover: { isVisible: false }, + meta: { showLock: false }, + state: { isEnabled: false }, + }, }; export default injectIntl(SequenceContent); diff --git a/src/courseware/course/sequence/Unit.jsx b/src/courseware/course/sequence/Unit.jsx index 78c53bec..74ade1ba 100644 --- a/src/courseware/course/sequence/Unit.jsx +++ b/src/courseware/course/sequence/Unit.jsx @@ -18,6 +18,8 @@ import { useModel } from '../../../generic/model-store'; import PageLoading from '../../../generic/PageLoading'; import { processEvent } from '../../../course-home/data/thunks'; import { fetchCourse } from '../../data/thunks'; +/** [MM-P2P] Experiment */ +import { MMP2PLockPaywall } from '../../../experiments/mm-p2p'; const LockPaywall = React.lazy(() => import('./lock-paywall')); const LockPaywallValuePropExperiment = React.lazy(() => import('./lock-paywall-value-prop')); @@ -60,6 +62,8 @@ function Unit({ onLoaded, id, intl, + /** [MM-P2P] Experiment */ + mmp2p, }) { const { authenticatedUser } = useContext(AppContext); const view = authenticatedUser ? 'student_view' : 'public_view'; @@ -130,7 +134,7 @@ function Unit({ isBookmarked={unit.bookmarked} isProcessing={unit.bookmarkedUpdateState === 'loading'} /> - {contentTypeGatingEnabled && unit.containsContentTypeGatedContent && ( + { !mmp2p.state.isEnabled && contentTypeGatingEnabled && unit.containsContentTypeGatedContent && ( } )} - {!hasLoaded && ( + { /** [MM-P2P] Experiment */ } + { mmp2p.meta.showLock && ( + + )} + { /** [MM-P2P] Experiment (conditional) */ } + {!mmp2p.meta.blockContent && !hasLoaded && ( @@ -173,24 +182,27 @@ function Unit({ dialogClassName="modal-lti" /> )} -
    -