Revert "MM-P2P G2 Activation (#380)" (#381)

This reverts commit 8c0cafafa1.
This commit is contained in:
Ben Warzeski
2021-03-10 18:55:34 -05:00
committed by GitHub
parent 8c0cafafa1
commit 5c65627582
36 changed files with 101 additions and 1603 deletions

24
package-lock.json generated
View File

@@ -1413,9 +1413,9 @@
}
},
"@edx/paragon": {
"version": "13.16.0",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-13.16.0.tgz",
"integrity": "sha512-E1XCpiHoD0TaTUV6o5FxfkxfUhtBmSsUCmR7LSTPXpiuI7ouK2PxbRoxCT8CnHbSAeeYKN0vPHNGEd9hFRm7zg==",
"version": "13.13.5",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-13.13.5.tgz",
"integrity": "sha512-6q60Lj5dbzZbcXfNrpHXqn4tKQYf1KmcISOe//un0JTSQr6/LnZjHZoZfmzDaRX/2KEDaLCTqCefvKTpB26qCQ==",
"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.1",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.1.tgz",
"integrity": "sha512-DvJbbn3dUgMxDnJLH+RZQPnXak1h4ZVYQ7CWiFWjQwBFkVajT4rfw2PdpHLTSTwxrYfnoEXkuBiwkDm6tPMQeA=="
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.0.tgz",
"integrity": "sha512-wjtKehFAIARq2OxK8j3JrggNlEslJfNuSm2ArteIbKyRMts2g0a7KzTxfRVNUM+O0gnBJ2hNV8nWPOYBgI1sew=="
},
"@reduxjs/toolkit": {
"version": "1.3.6",
@@ -3016,12 +3016,11 @@
"dev": true
},
"@types/react": {
"version": "17.0.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz",
"integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==",
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.2.tgz",
"integrity": "sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
@@ -3033,11 +3032,6 @@
"@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",

View File

@@ -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.16.0",
"@edx/paragon": "13.13.5",
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@fortawesome/free-brands-svg-icons": "5.13.1",
"@fortawesome/free-regular-svg-icons": "5.13.1",

View File

@@ -9,9 +9,6 @@ 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,
@@ -23,25 +20,19 @@ function DatesTab({ intl }) {
hasEnded,
} = useModel('dates', courseId);
/** [MM-P2P] Experiment */
const mmp2p = initDatesMMP2P(courseId);
return (
<>
<div role="heading" aria-level="1" className="h2 my-3">
{intl.formatMessage(messages.title)}
</div>
{ /** [MM-P2P] Experiment */ }
{ !mmp2p.state.isEnabled && (
<DatesBannerContainer
courseDateBlocks={courseDateBlocks}
datesBannerInfo={datesBannerInfo}
hasEnded={hasEnded}
model="dates"
tabFetch={fetchDatesTab}
/>
) }
<Timeline mmp2p={mmp2p} />
<DatesBannerContainer
courseDateBlocks={courseDateBlocks}
datesBannerInfo={datesBannerInfo}
hasEnded={hasEnded}
model="dates"
tabFetch={fetchDatesTab}
/>
<Timeline />
</>
);
}

View File

@@ -13,13 +13,7 @@ import { getBadgeListAndColor } from './badgelist';
import { isLearnerAssignment } from './utils';
function Day({
date,
first,
intl,
items,
last,
/** [MM-P2P] Example */
mmp2p,
date, first, intl, items, last,
}) {
const {
courseId,
@@ -32,11 +26,6 @@ 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 (
<li className="dates-day pb-4" data-testid="dates-day">
{/* Top Line */}
@@ -53,8 +42,7 @@ function Day({
<div className="mb-1" data-testid="dates-header">
<p className="d-inline text-dark-500 font-weight-bold">
<FormattedDate
/** [MM-P2P] Experiment */
value={mmp2pOverride ? mmp2p.state.upgradeDeadline : date}
value={date}
day="numeric"
month="short"
weekday="short"
@@ -65,16 +53,11 @@ function Day({
{badges}
</div>
{items.map((item) => {
/** [MM-P2P] Experiment (conditional) */
const { badges: itemBadges } = mmp2pOverride
? getBadgeListAndColor(new Date(mmp2p.state.upgradeDeadline), intl, item, items)
: getBadgeListAndColor(date, intl, item, items);
const { badges: itemBadges } = getBadgeListAndColor(date, intl, item, items);
const showLink = item.link && isLearnerAssignment(item);
const title = showLink ? (<u><a href={item.link} className="text-reset">{item.title}</a></u>) : item.title;
const available = item.learnerHasAccess && (item.link || !isLearnerAssignment(item));
const textColor = available ? 'text-dark-500' : 'text-dark-200';
return (
<div key={item.title + item.date} className={textColor} data-testid="dates-item">
<div>
@@ -93,15 +76,7 @@ function Day({
</OverlayTrigger>
)}
</div>
{ /** [MM-P2P] Experiment (conditional) */ }
{ mmp2pOverride
? (
<div className="small mb-2">
You are still elligible to upgrade to a Verified Certificate!
&nbsp; Unlock full course access and highlight the knowledge you&apos;ll gain.
</div>
)
: (item.description && <div className="small mb-2">{item.description}</div>)}
{item.description && <div className="small mb-2">{item.description}</div>}
</div>
);
})}
@@ -124,25 +99,11 @@ 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);

View File

@@ -1,6 +1,4 @@
import React from 'react';
/** [MM-P2P] Experiment (import) */
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useModel } from '../../generic/model-store';
@@ -8,8 +6,7 @@ import { useModel } from '../../generic/model-store';
import Day from './Day';
import { daycmp, isLearnerAssignment } from './utils';
/** [MM-P2P] Experiment (argument) */
export default function Timeline({ mmp2p }) {
export default function Timeline() {
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -66,17 +63,8 @@ export default function Timeline({ mmp2p }) {
return (
<ul className="list-unstyled m-0">
{groupedDates.map((groupedDate) => (
<Day key={groupedDate.date} {...groupedDate} mmp2p={mmp2p} />
<Day key={groupedDate.date} {...groupedDate} />
))}
</ul>
);
}
/** [MM-P2P] Experiment */
Timeline.propTypes = {
mmp2p: PropTypes.shape({}),
};
Timeline.defaultProps = {
mmp2p: {},
};

View File

@@ -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 from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { isLearnerAssignment } from '../dates-tab/utils';
import './DateSummary.scss';
@@ -9,14 +9,22 @@ 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 = mmp2p.state.isEnabled && (dateBlock.dateType === 'verified-upgrade-deadline');
/** [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');
return (
<li className="container p-0 mb-3 small text-dark-500">
@@ -24,8 +32,7 @@ export default function DateSummary({
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" fixedWidth />
<div className="ml-1 font-weight-bold">
<FormattedDate
/** [MM-P2P] Experiment */
value={showMMP2P ? mmp2p.state.upgradeDeadline : dateBlock.date}
value={dateBlock.date}
day="numeric"
month="short"
weekday="short"
@@ -34,8 +41,7 @@ export default function DateSummary({
/>
</div>
</div>
{/** [MM-P2P] Experiment (conditional) */}
{ showMMP2P ? (
{activateMMP2P ? (
<div className="row ml-4 pr-2">
<div className="date-summary-text">
<div className="font-weight-bold mt-2">
@@ -78,22 +84,8 @@ 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: '',
},
},
};

View File

@@ -28,9 +28,6 @@ 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,
@@ -91,9 +88,6 @@ function OutlineTab({ intl }) {
const courseSock = useRef(null);
/** [[MM-P2P] Experiment */
const MMP2P = initHomeMMP2P(courseId);
return (
<>
<Toast
@@ -115,8 +109,7 @@ function OutlineTab({ intl }) {
</div>
)}
</div>
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
<div className="row course-outline-tab">
<div className="row">
<div className="col-12">
<AlertList
topic="outline-private-alerts"
@@ -126,21 +119,17 @@ function OutlineTab({ intl }) {
/>
</div>
<div className="col col-12 col-md-8">
{ /** [MM-P2P] Experiment (the conditional) */ }
{ !MMP2P.state.isEnabled
&& (
<AlertList
topic="outline-course-alerts"
className="mb-3"
customAlerts={{
...accessExpirationAlert,
...certificateAvailableAlert,
...courseEndAlert,
...courseStartAlert,
...offerAlert,
}}
/>
)}
<AlertList
topic="outline-course-alerts"
className="mb-3"
customAlerts={{
...accessExpirationAlert,
...certificateAvailableAlert,
...courseEndAlert,
...courseStartAlert,
...offerAlert,
}}
/>
{courseDateBlocks && (
<DatesBannerContainer
courseDateBlocks={courseDateBlocks}
@@ -148,8 +137,6 @@ function OutlineTab({ intl }) {
hasEnded={hasEnded}
model="outline"
tabFetch={fetchOutlineTab}
/** [MM-P2P] Experiment */
isMMP2PEnabled={MMP2P.state.isEnabled}
/>
)}
{!courseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
@@ -202,21 +189,12 @@ function OutlineTab({ intl }) {
<CourseTools
courseId={courseId}
/>
{ /** [MM-P2P] Experiment (conditional) */ }
{ MMP2P.state.isEnabled
? <MMP2PFlyover isStatic options={MMP2P} />
: (
<UpgradeCard
courseId={courseId}
onLearnMore={
canShowUpgradeSock ? () => { courseSock.current.showToUser(); } : null
}
/>
)}
<UpgradeCard
courseId={courseId}
onLearnMore={canShowUpgradeSock ? () => { courseSock.current.showToUser(); } : null}
/>
<CourseDates
courseId={courseId}
/** [MM-P2P] Experiment */
mmp2p={MMP2P}
/>
<CourseHandouts
courseId={courseId}

View File

@@ -7,12 +7,7 @@ import DateSummary from '../DateSummary';
import messages from '../messages';
import { useModel } from '../../../generic/model-store';
function CourseDates({
courseId,
intl,
/** [MM-P2P] Experiment */
mmp2p,
}) {
function CourseDates({ courseId, intl }) {
const {
datesWidget: {
courseDateBlocks,
@@ -34,8 +29,6 @@ function CourseDates({
key={courseDateBlock.title + courseDateBlock.date}
dateBlock={courseDateBlock}
userTimezone={userTimezone}
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
/>
))}
</ol>
@@ -49,14 +42,10 @@ function CourseDates({
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);

View File

@@ -16,9 +16,6 @@ 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,
@@ -86,25 +83,19 @@ function Course({
};
// The above block of code should be reverted after the REV1512 experiment
/** [MM-P2P] Experiment */
const MMP2P = initCoursewareMMP2P(courseId, sequenceId, unitId);
return (
<>
<Helmet>
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
</Helmet>
{ /** This conditional is for the [MM-P2P] Experiment */}
{ !MMP2P.state.isEnabled && (
<AlertList
className="my-3"
topic="course"
customAlerts={{
...accessExpirationAlert,
...offerAlert,
}}
/>
)}
<AlertList
className="my-3"
topic="course"
customAlerts={{
...accessExpirationAlert,
...offerAlert,
}}
/>
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
@@ -112,8 +103,6 @@ function Course({
toggleREV1512Flyover={toggleREV1512Flyover} /* This line should be reverted after REV1512 experiment */
REV1512FlyoverEnabled={REV1512FlyoverEnabled} /* This line should be reverted after REV1512 experiment */
isREV1512FlyoverVisible={isREV1512FlyoverVisible} /* This line should be reverted after REV1512 experiment */
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}
/>
<AlertList topic="sequence" />
<Sequence
@@ -126,8 +115,6 @@ function Course({
toggleREV1512Flyover={toggleREV1512Flyover} /* This line should be reverted after REV1512 experiment */
isREV1512FlyoverVisible={isREV1512FlyoverVisible} /* This line should be reverted after REV1512 experiment */
REV1512FlyoverEnabled={REV1512FlyoverEnabled} /* This line should be reverted after REV1512 experiment */
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}
/>
{celebrationOpen && (
<CelebrationModal
@@ -145,8 +132,6 @@ function Course({
/>
)}
<ContentTools course={course} />
{ /** [MM-P2P] Experiment */ }
{ MMP2P.meta.modalLock && <MMP2PBlockModal options={MMP2P} /> }
</>
);
}

View File

@@ -7,9 +7,6 @@ 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
}) {
@@ -42,8 +39,6 @@ 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);
@@ -97,24 +92,20 @@ export default function CourseBreadcrumbs({
</CourseBreadcrumb>
))}
{/* The below block of code should be reverted after the REV1512 experiment */}
{/** [MM-P2P] Experiment (additional conditional) */}
{REV1512FlyoverEnabled && !mmp2p.state.isEnabled && !isMobile && (
<div
className="toggleFlyoverButton"
aria-hidden="true"
style={{ marginLeft: 'auto', marginTop: '-16px', borderBottom: isREV1512FlyoverVisible() ? '2px solid #00262b' : 'none' }}
onClick={() => {
toggleREV1512Flyover();
}}
>
<svg width="54" height="40" viewBox="0 0 54 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="53" height="39" rx="1.5" fill="white" stroke="#E7E8E9" /><path d="M36 20C36 15.6 32.4 12 28 12C27.7 12 27.5 12.2 27.5 12.5C27.5 12.8 27.7 13 28 13C31.85 13 35 16.15 35 20C35 23.85 31.85 27 28 27C24.15 27 21 23.85 21 20C21 19.7 20.8 19.5 20.5 19.5C20.3 19.5 20.1 19.65 20.05 19.8C20 19.85 20 19.95 20 20C20 24.4 23.6 28 28 28C32.4 28 36 24.4 36 20Z" fill="black" stroke="black" strokeWidth="0.6" /><path d="M23.1065 14.52C22.9403 14.36 22.691 14.36 22.5247 14.52C22.3585 14.68 22.3585 14.92 22.5247 15.08C22.6078 15.16 22.7325 15.2 22.8156 15.2C22.9403 15.2 23.0234 15.16 23.1065 15.08C23.2312 14.96 23.2312 14.68 23.1065 14.52Z" fill="black" stroke="black" strokeWidth="0.6" /><path d="M27.6848 15.2C27.3939 15.2 27.2 15.3973 27.2 15.6932V19.6384C27.2 19.6877 27.2 19.7863 27.2484 19.8356C27.2969 19.8849 27.2969 19.9343 27.3454 19.9836L29.5757 22.2521C29.6727 22.3507 29.8181 22.4 29.9151 22.4C30.0121 22.4 30.1575 22.3507 30.2545 22.2521C30.4484 22.0548 30.4484 21.7589 30.2545 21.5617L28.1696 19.4411V15.6932C28.1696 15.3973 27.9757 15.2 27.6848 15.2Z" fill="black" stroke="black" strokeWidth="0.6" /><circle cx="35.5" cy="14.5" r="4.5" fill="#C32D3A" />
</svg>
</div>
)}
{/** [MM-P2P] Experiment */}
{mmp2p.state.isEnabled && (
<MMP2PFlyoverTrigger options={mmp2p} />
{REV1512FlyoverEnabled
&& !isMobile && (
<div
className="toggleFlyoverButton"
aria-hidden="true"
style={{ marginLeft: 'auto', marginTop: '-16px', borderBottom: isREV1512FlyoverVisible() ? '2px solid #00262b' : 'none' }}
onClick={() => {
toggleREV1512Flyover();
}}
>
<svg width="54" height="40" viewBox="0 0 54 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="53" height="39" rx="1.5" fill="white" stroke="#E7E8E9" /><path d="M36 20C36 15.6 32.4 12 28 12C27.7 12 27.5 12.2 27.5 12.5C27.5 12.8 27.7 13 28 13C31.85 13 35 16.15 35 20C35 23.85 31.85 27 28 27C24.15 27 21 23.85 21 20C21 19.7 20.8 19.5 20.5 19.5C20.3 19.5 20.1 19.65 20.05 19.8C20 19.85 20 19.95 20 20C20 24.4 23.6 28 28 28C32.4 28 36 24.4 36 20Z" fill="black" stroke="black" strokeWidth="0.6" /><path d="M23.1065 14.52C22.9403 14.36 22.691 14.36 22.5247 14.52C22.3585 14.68 22.3585 14.92 22.5247 15.08C22.6078 15.16 22.7325 15.2 22.8156 15.2C22.9403 15.2 23.0234 15.16 23.1065 15.08C23.2312 14.96 23.2312 14.68 23.1065 14.52Z" fill="black" stroke="black" strokeWidth="0.6" /><path d="M27.6848 15.2C27.3939 15.2 27.2 15.3973 27.2 15.6932V19.6384C27.2 19.6877 27.2 19.7863 27.2484 19.8356C27.2969 19.8849 27.2969 19.9343 27.3454 19.9836L29.5757 22.2521C29.6727 22.3507 29.8181 22.4 29.9151 22.4C30.0121 22.4 30.1575 22.3507 30.2545 22.2521C30.4484 22.0548 30.4484 21.7589 30.2545 21.5617L28.1696 19.4411V15.6932C28.1696 15.3973 27.9757 15.2 27.6848 15.2Z" fill="black" stroke="black" strokeWidth="0.6" /><circle cx="35.5" cy="14.5" r="4.5" fill="#C32D3A" />
</svg>
</div>
)}
</ol>
</nav>
@@ -128,19 +119,9 @@ 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: {},
};

View File

@@ -24,9 +24,6 @@ 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 (
@@ -195,8 +192,6 @@ 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);
@@ -323,9 +318,6 @@ 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();
@@ -347,8 +339,6 @@ function Sequence({
sequenceId={sequenceId}
unitId={unitId}
unitLoadedHandler={handleUnitLoaded}
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
/>
{unitHasLoaded && (
<UnitNavigation
@@ -368,18 +358,11 @@ function Sequence({
</div>
</div>
{/* This block of code should be reverted post REV1512 experiment */}
{/** [MM-P2P] Experiment (additional conditional) */}
{!mmp2p.state.isEnabled && REV1512FlyoverEnabled && isREV1512FlyoverVisible() && (
{REV1512FlyoverEnabled && isREV1512FlyoverVisible() && (
isMobile
? <REV1512FlyoverMobile toggleREV1512Flyover={toggleREV1512Flyover} />
: <REV1512Flyover toggleREV1512Flyover={toggleREV1512Flyover} />
)}
{/** [MM-P2P] Experiment */}
{(mmp2p.state.isEnabled && mmp2p.flyover.isVisible) && (
isMobile
? <MMP2PFlyoverMobile options={mmp2p} />
: <MMP2PFlyover options={mmp2p} />
)}
</div>
<CourseLicense license={course.license || undefined} />
</div>
@@ -405,31 +388,11 @@ 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);

View File

@@ -10,14 +10,7 @@ import Unit from './Unit';
const ContentLock = React.lazy(() => import('./content-lock'));
function SequenceContent({
gated,
intl,
courseId,
sequenceId,
unitId,
unitLoadedHandler,
/** [MM-P2P] Experiment */
mmp2p,
gated, intl, courseId, sequenceId, unitId, unitLoadedHandler,
}) {
const sequence = useModel('sequences', sequenceId);
@@ -61,8 +54,6 @@ function SequenceContent({
key={unitId}
id={unitId}
onLoaded={unitLoadedHandler}
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
/>
);
}
@@ -74,28 +65,10 @@ 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);

View File

@@ -18,8 +18,6 @@ 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'));
@@ -62,8 +60,6 @@ function Unit({
onLoaded,
id,
intl,
/** [MM-P2P] Experiment */
mmp2p,
}) {
const { authenticatedUser } = useContext(AppContext);
const view = authenticatedUser ? 'student_view' : 'public_view';
@@ -134,7 +130,7 @@ function Unit({
isBookmarked={unit.bookmarked}
isProcessing={unit.bookmarkedUpdateState === 'loading'}
/>
{ !mmp2p.state.isEnabled && contentTypeGatingEnabled && unit.containsContentTypeGatedContent && (
{contentTypeGatingEnabled && unit.containsContentTypeGatedContent && (
<Suspense
fallback={(
<PageLoading
@@ -147,12 +143,7 @@ function Unit({
: <LockPaywall courseId={courseId} />}
</Suspense>
)}
{ /** [MM-P2P] Experiment */ }
{ mmp2p.meta.showLock && (
<MMP2PLockPaywall options={mmp2p} />
)}
{ /** [MM-P2P] Experiment (conditional) */ }
{!mmp2p.meta.blockContent && !hasLoaded && (
{!hasLoaded && (
<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
/>
@@ -182,27 +173,24 @@ function Unit({
dialogClassName="modal-lti"
/>
)}
{ /** [MM-P2P] Experiment (conditional) */ }
{ !mmp2p.meta.blockContent && (
<div className="unit-iframe-wrapper">
<iframe
id="unit-iframe"
title={unit.title}
src={iframeUrl}
allowFullScreen
height={iframeHeight}
scrolling="no"
referrerPolicy="origin"
onLoad={() => {
window.onmessage = function handleResetDates(e) {
if (e.data.event_name) {
dispatch(processEvent(e.data, fetchCourse));
}
};
}}
/>
</div>
)}
<div className="unit-iframe-wrapper">
<iframe
id="unit-iframe"
title={unit.title}
src={iframeUrl}
allowFullScreen
height={iframeHeight}
scrolling="no"
referrerPolicy="origin"
onLoad={() => {
window.onmessage = function handleResetDates(e) {
if (e.data.event_name) {
dispatch(processEvent(e.data, fetchCourse));
}
};
}}
/>
</div>
</div>
);
}
@@ -213,31 +201,11 @@ Unit.propTypes = {
id: PropTypes.string.isRequired,
intl: intlShape.isRequired,
onLoaded: PropTypes.func,
/** [MM-P2P] Experiment */
mmp2p: PropTypes.shape({
state: PropTypes.shape({
isEnabled: PropTypes.bool.isRequired,
}),
meta: PropTypes.shape({
showLock: PropTypes.bool,
blockContent: PropTypes.bool,
}),
}),
};
Unit.defaultProps = {
format: null,
onLoaded: undefined,
/** [MM-P2P] Experiment */
mmp2p: {
state: {
isEnabled: false,
},
meta: {
showLock: false,
blockContent: false,
},
},
};
export default injectIntl(Unit);

View File

@@ -15,8 +15,6 @@ import { useModel } from '../../../../generic/model-store';
import { LOADED } from '../../../data/slice';
import messages from './messages';
/** [MM-P2P] Experiment */
import { MMP2PFlyoverTriggerMobile } from '../../../../experiments/mm-p2p';
function SequenceNavigation({
intl,
@@ -30,8 +28,6 @@ function SequenceNavigation({
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 sequence = useModel('sequences', sequenceId);
const { isFirstUnit, isLastUnit } = useSequenceNavigationMetadata(sequenceId, unitId);
@@ -91,8 +87,7 @@ function SequenceNavigation({
</Button>
{renderUnitButtons()}
{renderNextButton()}
{/** [MM-P2P] Experiment (additional conditional) */}
{!mmp2p.state.isEnabled && REV1512FlyoverEnabled
{REV1512FlyoverEnabled
&& isMobile && (
<div
className="toggleFlyoverButton toggleFlyoverButtonMobile"
@@ -107,8 +102,6 @@ function SequenceNavigation({
</svg>
</div>
)}
{/** [MM-P2P] Experiment */}
{ mmp2p.state.isEnabled && <MMP2PFlyoverTriggerMobile options={mmp2p} /> }
<div className="rev1512ToggleFlyoverSequenceLocation" />
</nav>
);
@@ -126,23 +119,11 @@ SequenceNavigation.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,
}),
}),
};
SequenceNavigation.defaultProps = {
className: null,
unitId: null,
/** [MM-P2P] Experiment */
mmp2p: {
state: { isEnabled: false },
},
};
export default injectIntl(SequenceNavigation);

View File

@@ -1,21 +0,0 @@
import React, { Suspense } from 'react';
import { ModalLayer } from '@edx/paragon';
import PageLoading from '../../generic/PageLoading';
const BlockModalContent = React.lazy(() => import('./BlockModalContent'));
export const BlockModal = () => (
<ModalLayer
isOpen
onClose={() => {}}
isBlocking
>
<Suspense fallback={(<PageLoading srMessage="Loading blocked content modal" />)}>
<BlockModalContent />
</Suspense>
</ModalLayer>
);
export default BlockModal;

View File

@@ -1,43 +0,0 @@
.mmp2p-modal-dialog.modal-content {
padding: 15px;
border-radius: 0px;
.mmp2p-block-modal-wrapper {
background-color: white;
text-align: left;
.bullet-list-item {
font-size: 18px;
line-height: 28px;
font-weight: 400;
&:not(:last-child) {
margin-bottom: 20px;
}
color: $gray-900;
.icon-container {
vertical-align: top;
display: inline-block;
margin-right: 7px;
}
.bullet-item-content {
display: inline-block;
width: calc(100% - 245px);
}
}
.subheader {
font-size: 18px;
line-height: 28px;
font-weight: 500;
margin-bottom: 15px;
}
img.certificate-image {
position: absolute;
top: 32px;
right: 32px;
width: 184px;
}
#mmp2p-modal-explore-btn {
float: right;
margin-bottom: 16px;
margin-right: 16px;
}
}
}

View File

@@ -1,78 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, ModalLayer } from '@edx/paragon';
import CertImage from '../../generic/assets/edX_certificate.png';
const BulletList = ({ children }) => (
<div className="bullet-list-item">
<div className="icon-container">
<svg
aria-hidden="true"
focusable="false"
data-prefix="far"
data-icon="check-circle"
className="svg-inline--fa fa-check-circle fa-w-16 mmp2p-bullet-list"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z"
/>
</svg>
</div>
<div className="bullet-item-content">
{children}
</div>
</div>
);
BulletList.propTypes = {
children: PropTypes.node.isRequired,
};
export const BlockModal = () => (
<ModalLayer
isOpen
onClose={() => {}}
isBlocking
>
<div className="mmp2p-modal-dialog modal-content modal-xl">
<div className="mmp2p-block-modal-wrapper">
<h3>
Deadline to access full course has passed
</h3>
<div className="subheader">
What does the Verified Track get you?
</div>
<div>
<BulletList>
Earn a verified certificate of completion to showcase on your resume
</BulletList>
<BulletList>
Unlock unlimited access to all course content and activities,
&nbsp;including graded assignments, even after the course ends.
</BulletList>
<BulletList>
Support our non-profit mission at edx
</BulletList>
</div>
<img src={CertImage} className="certificate-image" alt="Example Certificate" />
<Button
id="mmp2p-modal-explore-btn"
variant="brand"
href="https://www.edx.org/search"
data-ol-has-click-handler=""
style={{ fontSize: '1em', fontWeight: 600 }}
>
Explore more courses
</Button>
</div>
</div>
</ModalLayer>
);
export default BlockModal;

View File

@@ -1,83 +0,0 @@
/* eslint-disable no-use-before-define */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Sidecard from './Sidecard';
const Flyover = ({
isStatic,
options,
}) => {
const handleHideFlyoverKeyPress = (event) => {
if (event.key === 'Enter') {
options.flyover.toggle();
}
};
if (!options.access.isAudit || options.state.afterUpgradeDeadline) {
return null;
}
return (
<div className={classNames('mmp2p-flyover', { static: isStatic })}>
{ !isStatic && (
<div className="mmp2p-notification-div">
<span>Notifications</span>
<span
onClick={options.flyover.toggle}
className="mmp2p-hide-flyover"
onKeyPress={handleHideFlyoverKeyPress}
role="button"
tabIndex={0}
>
<svg
className="mmp2p-flyover-icon"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.60625 7L13.5152 3.09102C13.9949 2.61133 13.9949 1.83359 13.5152 1.35352L12.6465 0.484766C12.1668 0.00507814 11.3891 0.00507814 10.909 0.484766L7 4.39375L3.09102 0.484766C2.61133 0.00507814 1.83359 0.00507814 1.35352 0.484766L0.484766 1.35352C0.00507814 1.8332 0.00507814 2.61094 0.484766 3.09102L4.39375 7L0.484766 10.909C0.00507814 11.3887 0.00507814 12.1664 0.484766 12.6465L1.35352 13.5152C1.8332 13.9949 2.61133 13.9949 3.09102 13.5152L7 9.60625L10.909 13.5152C11.3887 13.9949 12.1668 13.9949 12.6465 13.5152L13.5152 12.6465C13.9949 12.1668 13.9949 11.3891 13.5152 10.909L9.60625 7Z"
fill="black"
/>
</svg>
</span>
<div className="mmp2p-notification-block" />
</div>
)}
<Sidecard options={options} />
</div>
);
};
Flyover.propTypes = {
isStatic: PropTypes.bool,
options: PropTypes.shape({
access: PropTypes.shape({
isAudit: PropTypes.bool.isRequired,
}),
flyover: PropTypes.shape({
toggle: PropTypes.func.isRequired,
}),
state: PropTypes.shape({
afterUpgradeDeadline: PropTypes.bool.isRequired,
}),
}),
};
Flyover.defaultProps = {
isStatic: false,
options: {
access: {
isAudit: false,
},
flyover: {
toggle: () => {},
},
state: {
afterUpgradeDeadline: false,
},
},
};
export default Flyover;

View File

@@ -1,38 +0,0 @@
@media only screen and (min-width: 600px) {
.mmp2p-flyover {
min-width: 315px !important;
h4 {
font-size: 16px;
}
}
}
.mmp2p-flyover {
&:not(.static) {
height: 100% !important;
height: 393px;
}
&.static {
margin-bottom: 20px;
}
border: solid 1px #e1dddb;
width: 330px;
vertical-align: top;
margin-left: 20px;
padding: 0 20px 20px 20px;
.mmp2p-notification-div {
margin: 0 -20px 0px;
padding: 9px 20px 0;
font-size: 16px;
}
.mmp2p-notification-block {
height: 9px;
background: #F9F9F9;
margin: 7px -20px 0;
border-top: 1px solid rgb(225, 221, 219);
border-bottom: 1px solid rgb(225, 221, 219);
}
svg.mmp2p-flyover-icon {
float: right;
margin-top: 5.5px;
}
}

View File

@@ -1,86 +0,0 @@
/* eslint-disable no-use-before-define */
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import Sidecard from './Sidecard';
export const FlyoverMobile = ({ options }) => {
const {
access: { isAudit },
flyover: { toggle },
state: { afterUpgradeDeadline },
} = options;
const handleReturnSpanKeyPress = (event) => {
if (event.key === 'Enter') {
toggle();
}
};
if (!isAudit || afterUpgradeDeadline) {
return null;
}
return (
<div className="mmp2p-flyover-mobile">
<div className="mmp2p-mobile-return-div">
<span
className="mmp2p-mobile-return-span"
onClick={toggle}
onKeyPress={handleReturnSpanKeyPress}
role="button"
tabIndex={0}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="mr-2 fa-lg"
style={{ marginBottom: 2 }}
/>
<span className="mmp2p-mobile-return-text">
Back to course
</span>
</span>
</div>
<div className="mmp2p-notification-div">
<span className="mmp2p-notification-span">
Notifications
</span>
<div className="mmp2p-notification-block">
<Sidecard options={options} />
</div>
</div>
</div>
);
};
FlyoverMobile.propTypes = {
options: PropTypes.shape({
access: PropTypes.shape({
isAudit: PropTypes.bool.isRequired,
}),
flyover: PropTypes.shape({
toggle: PropTypes.func.isRequired,
}),
state: PropTypes.shape({
afterUpgradeDeadline: PropTypes.bool.isRequired,
}),
}),
};
FlyoverMobile.defaultProps = {
options: {
access: {
isAudit: false,
},
flyover: {
toggle: () => {},
},
state: {
afterUpgradeDeadline: false,
},
},
};
export default FlyoverMobile;

View File

@@ -1,37 +0,0 @@
.mmp2p-flyover-mobile {
vertical-align: top;
padding: 0 20px 20px 20px;
position: fixed;
background-color: white;
z-index: 1;
height: 100%;
width: 100%;
top: 0;
left: 0;
.mmp2p-mobile-return-div {
margin: 0 -20px;
padding: 9px 20px 15px;
font-size: 16px;
border-bottom: 1px solid rgb(225, 221, 219);
}
.mmp2p-mobile-return-span {
color: #00262B;
cursor: pointer;
}
.mmp2p-notification-div {
margin: 0 -20px 15px;
padding: 9px 20px 0;
font-size: 16px;
}
.mmp2p-notification-span {
color: #00262B;
}
.mmp2p-notification-block {
height: 9px;
background: #F9F9F9;
margin: 7px -20px 0;
border-top: 1px solid rgb(225, 221, 219);
border-bottom: 1px solid rgb(225, 221, 219);
}
}

View File

@@ -1,58 +0,0 @@
/* eslint-disable no-use-before-define */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import FlyoverTriggerIcon from './FlyoverTriggerIcon';
import { isMobile } from './utils';
export const FlyoverTrigger = ({ options }) => {
const { isVisible, toggle } = options.flyover;
if (!options.access.isAudit || options.state.afterUpgradeDeadline) {
return null;
}
return (!isMobile() && (
<div
className={classNames(
'mmp2p-toggle-flyover-button',
{ 'flyover-visible': isVisible },
)}
aria-hidden="true"
onClick={toggle}
>
<FlyoverTriggerIcon />
</div>
));
};
FlyoverTrigger.propTypes = {
options: PropTypes.shape({
access: PropTypes.shape({
isAudit: PropTypes.bool.isRequired,
}),
flyover: PropTypes.shape({
isVisible: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
}),
state: PropTypes.shape({
afterUpgradeDeadline: PropTypes.bool.isRequired,
isEnabled: PropTypes.bool.isRequired,
}),
}),
};
FlyoverTrigger.defaultProps = {
options: {
access: { isAudit: false },
flyover: {
isVisible: false,
toggle: () => {},
},
state: {
afterUpgradeDeadline: false,
isEnabled: false,
},
},
};
export default FlyoverTrigger;

View File

@@ -1,8 +0,0 @@
.mmp2p-toggle-flyover-button {
margin-left: auto;
margin-top: 0px !important;
border-bottom: none;
&.flyover-visible {
border-bottom: 2px solid #00262b;
}
}

View File

@@ -1,48 +0,0 @@
/* eslint-disable no-use-before-define */
import React from 'react';
const FlyoverTriggerIcon = () => (
<svg
width="54"
height="40"
viewBox="0 0 54 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="53"
height="39"
rx="1.5"
fill="white"
stroke="#E7E8E9"
/>
<path
d="M36 20C36 15.6 32.4 12 28 12C27.7 12 27.5 12.2 27.5 12.5C27.5 12.8 27.7 13 28 13C31.85 13 35 16.15 35 20C35 23.85 31.85 27 28 27C24.15 27 21 23.85 21 20C21 19.7 20.8 19.5 20.5 19.5C20.3 19.5 20.1 19.65 20.05 19.8C20 19.85 20 19.95 20 20C20 24.4 23.6 28 28 28C32.4 28 36 24.4 36 20Z"
fill="black"
stroke="black"
strokeWidth="0.6"
/>
<path
d="M23.1065 14.52C22.9403 14.36 22.691 14.36 22.5247 14.52C22.3585 14.68 22.3585 14.92 22.5247 15.08C22.6078 15.16 22.7325 15.2 22.8156 15.2C22.9403 15.2 23.0234 15.16 23.1065 15.08C23.2312 14.96 23.2312 14.68 23.1065 14.52Z"
fill="black"
stroke="black"
strokeWidth="0.6"
/>
<path
d="M27.6848 15.2C27.3939 15.2 27.2 15.3973 27.2 15.6932V19.6384C27.2 19.6877 27.2 19.7863 27.2484 19.8356C27.2969 19.8849 27.2969 19.9343 27.3454 19.9836L29.5757 22.2521C29.6727 22.3507 29.8181 22.4 29.9151 22.4C30.0121 22.4 30.1575 22.3507 30.2545 22.2521C30.4484 22.0548 30.4484 21.7589 30.2545 21.5617L28.1696 19.4411V15.6932C28.1696 15.3973 27.9757 15.2 27.6848 15.2Z"
fill="black"
stroke="black"
strokeWidth="0.6"
/>
<circle
cx="35.5"
cy="14.5"
r="4.5"
fill="#C32D3A"
/>
</svg>
);
export default FlyoverTriggerIcon;

View File

@@ -1,54 +0,0 @@
/* eslint-disable no-use-before-define */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import FlyoverTriggerIcon from './FlyoverTriggerIcon';
import { isMobile } from './utils';
const FlyoverTriggerMobile = ({ options }) => {
const { isVisible, toggle } = options.flyover;
if (!options.access.isAudit || options.state.afterUpgradeDeadline) {
return null;
}
return (isMobile() && (
<div
className={classNames(
'mmp2p-toggle-flyover-button-mobile',
{ 'flyover-visible': isVisible },
)}
aria-hidden="true"
onClick={toggle}
>
<FlyoverTriggerIcon />
</div>
));
};
FlyoverTriggerMobile.propTypes = {
options: PropTypes.shape({
access: PropTypes.shape({
isAudit: PropTypes.bool.isRequired,
}),
flyover: PropTypes.shape({
isVisible: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
}),
state: PropTypes.shape({
afterUpgradeDeadline: PropTypes.bool.isRequired,
}),
}),
};
FlyoverTriggerMobile.defaultProps = {
options: {
access: { isAudit: false },
flyover: {
isVisible: true,
toggle: () => {},
},
state: { afterUpgradeDeadline: false },
},
};
export default FlyoverTriggerMobile;

View File

@@ -1,7 +0,0 @@
.mmp2p-toggle-flyover-button-mobile {
border-bottom: none;
margin-left: 10px !important;
&.flyover-visible {
border-bottom: 2px solid #00262b;
}
}

View File

@@ -1,45 +0,0 @@
import React, { Suspense } from 'react';
import PropTypes from 'prop-types';
import PageLoading from '../../generic/PageLoading';
const LockPaywallContent = React.lazy(() => import('./LockPaywallContent'));
const LockPaywall = ({ options }) => {
if (!(options.meta.gradedLock || options.meta.verifiedLock)) {
return null;
}
return (
<Suspense
fallback={(<PageLoading srMessage="Loading locked content messaging..." />)}
>
<LockPaywallContent options={options} />
</Suspense>
);
};
LockPaywall.propTypes = {
options: PropTypes.shape({
access: PropTypes.shape({
upgradeUrl: PropTypes.string.isRequired,
price: PropTypes.string.isRequired,
}),
meta: PropTypes.shape({
gradedLock: PropTypes.bool.isRequired,
verifiedLock: PropTypes.bool.isRequired,
}),
}),
};
LockPaywall.defaultProps = {
options: {
access: {
upgradeUrl: '',
price: '$23',
},
meta: {
gradedLock: false,
verifiedLock: false,
},
},
};
export default LockPaywall;

View File

@@ -1,73 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import VerifiedCert from '../../generic/assets/edX_certificate.png';
const LockPaywallContent = ({ options }) => (
<div className="border border-gray rounded d-flex justify-content-between mt-2 p-3">
<div>
<h4 className="font-weight-bold mb-2">
<FontAwesomeIcon
icon={faLock}
className="text-black mr-2 ml-1"
style={{ fontSize: '2rem' }}
/>
<span>Verified Track Access</span>
</h4>
<p className="mb-0">
<span>
{ options.meta.gradedLock && (
<>Grades assessments are available to Verified Track learners.</>
)}
{ options.meta.verifiedLock && (
<>
Audit access is limited to the first two weeks of scheduled content.
&nbsp; To access the full course content,
</>
)}
</span>
&nbsp;
<a className="lock_paywall_upgrade_link" href={options.access.upgradeUrl}>
{ options.meta.gradedLock ? 'Upgrade to unlock ' : 'upgrade to a verified certificate.' }
({options.access.price})
</a>
</p>
</div>
<div>
<img
alt="Example Certificate"
src={VerifiedCert}
className="border-0"
style={{ height: '70px' }}
/>
</div>
</div>
);
LockPaywallContent.propTypes = {
options: PropTypes.shape({
access: PropTypes.shape({
upgradeUrl: PropTypes.string.isRequired,
price: PropTypes.string.isRequired,
}),
meta: PropTypes.shape({
gradedLock: PropTypes.bool.isRequired,
verifiedLock: PropTypes.bool.isRequired,
}),
}),
};
LockPaywallContent.defaultProps = {
options: {
access: {
upgradeUrl: '',
price: '$23',
},
meta: {
gradedLock: false,
verifiedLock: false,
},
},
};
export default LockPaywallContent;

View File

@@ -1,12 +0,0 @@
# What is this experiment?
This is an experiment updating the audit experience for specific micromasters courses, limiting
available access to content to the 1st 2 weeks of content, and restricting the upgrade deadline.
# What is the audience?
All students in a small number of specific Micro-Masters courses
# Who owns it?
Sapana Thomas and the Masters-Grades team
# Who is responsible for cleaning up this code?
The Masters-Grades team is on-tap for cleaning up this code at the end of the experiment

View File

@@ -1,42 +0,0 @@
import React, { Suspense } from 'react';
import PropTypes from 'prop-types';
import PageLoading from '../../generic/PageLoading';
const SidecardContent = React.lazy(() => import('./SidecardContent'));
const Sidecard = ({ options }) => (
<Suspense
fallback={(<PageLoading srMessage="Loading upgrade messaging..." />)}
>
<SidecardContent options={options} />
</Suspense>
);
Sidecard.propTypes = {
options: PropTypes.shape({
state: PropTypes.shape({
upgradeDeadline: PropTypes.string.isRequired,
}),
access: PropTypes.shape({
accessExpirationDate: PropTypes.string.isRequired,
price: PropTypes.string.isRequired,
upgradeUrl: PropTypes.string.isRequired,
}),
}),
};
Sidecard.defaultProps = {
options: {
state: {
upgradeDeadline: 'Mar 29, 2021 11:59 PM EST',
},
access: {
accessDeadline: 'Mar 21, 2022 11:59 PM EST',
price: '$23',
upgradeUrl: '',
},
},
};
export default Sidecard;

View File

@@ -1,53 +0,0 @@
.mmp2p-sidecard-wrapper {
padding-top: 15px;
.cert-link {
font-weight: 600;
color: #00688D;
text-decoration: 'underline',
}
.mmp2p-bullet-list-item {
.icon-container {
vertical-align: top;
display: inline-block;
margin-right: 7px;
}
.bullet-item-content {
display: inline-block;
width: calc(100% - 26px);
}
svg {
font-size: 14px;
margin-right: 5px;
}
}
.mmp2p-sidecard-alert {
margin-left: -20px;
margin-right: -20px;
padding-top: 1rem;
padding-bottom: 1rem;
margin-top: 15px;
line-height: 1;
background-color: #fffadb;
color: black;
font-size: 14px;
border-radius: 0px !important;
&.danger {
background-color: #fcf1f4;
}
}
.mmp2p-coupon-code {
border: solid 1px #e1dddb;
padding: 20px;
margin: -21px;
background: #fbfaf9;
font-size: 14px;
text-align: center;
}
.verification-sock {
display: none;
}
.alert-info {
display: none;
}
}

View File

@@ -1,169 +0,0 @@
/* eslint-disable no-use-before-define */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const AlertBanner = ({ color, children }) => (
<div
className={classNames(
'mmp2p-sidecard-alert alert',
{ danger: color === 'red' },
)}
>
{children}
</div>
);
AlertBanner.propTypes = {
color: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
const localizeTime = (date) => date.toLocaleTimeString('en-US',
{
hour: '2-digit', minute: 'numeric', hour12: true, timeZoneName: 'short',
});
const localizeDate = (date) => date.toLocaleDateString('en-US',
{ month: 'long', day: 'numeric' });
const BulletList = ({ children }) => (
<div style={{ marginBottom: '3px' }} className="mmp2p-bullet-list-item">
<div className="icon-container">
<svg
aria-hidden="true"
focusable="false"
data-prefix="far"
data-icon="check-circle"
className="svg-inline--fa fa-check-circle fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z"
/>
</svg>
</div>
<div className="bullet-item-content">
{children}
</div>
</div>
);
BulletList.propTypes = {
children: PropTypes.node.isRequired,
};
const Sidecard = ({
options: {
state: { upgradeDeadline },
access: { accessExpirationDate, price, upgradeUrl },
},
}) => {
const dates = {
upgradeDeadline: new Date(upgradeDeadline),
accessExpirationDate: new Date(accessExpirationDate),
now: new Date(),
};
const upgradeDeadlineTime = localizeTime(dates.upgradeDeadline);
const upgradeDeadlineDate = localizeDate(dates.upgradeDeadline);
const daysUntilDeadline = parseInt((dates.upgradeDeadline - dates.now) / (1000 * 60 * 60 * 24), 10);
const hoursUntilDeadline = parseInt((dates.upgradeDeadline - dates.now) / (1000 * 60 * 60), 10);
const accessDeadlineDate = localizeDate(dates.accessExpirationDate);
const certLink = (
<span className="cert-link">
<a
id="mmp2p-support-link"
href="https://www.edx.org/verified-certificate"
target="_blank"
rel="noopener noreferrer"
>
verified certificate
</a>
</span>
);
return (
<div className="mmp2p-sidecard-wrapper section">
<h5 className="hd hd-6">
Unlock the full course by {upgradeDeadlineDate} at {upgradeDeadlineTime}
</h5>
<AlertBanner color={daysUntilDeadline >= 7 ? 'yellow' : 'red'}>
{(daysUntilDeadline > 1) && `${daysUntilDeadline} days left`}
{(daysUntilDeadline === 1) && '1 day left'}
{(daysUntilDeadline < 1 && hoursUntilDeadline >= 1) && `${hoursUntilDeadline} hours left`}
{(daysUntilDeadline < 1 && hoursUntilDeadline < 1) && 'Less than one hour left'}
</AlertBanner>
<div style={{ fontSize: '14px' }}>
<BulletList>
Unlock your access to all course activities, including
&nbsp;<span style={{ fontWeight: 600 }}>graded assignments</span>
</BulletList>
<BulletList>
Earn a {certLink} of completion to showcase on your resume
</BulletList>
<BulletList>
Support our <span style={{ fontWeight: 600 }}>non-profit mission</span> at edX
</BulletList>
</div>
<div style={{ fontSize: '12px', marginTop: '10px', marginBottom: '5px' }}>
You will lose access to the first two weeks of scheduled content on {accessDeadlineDate}.
</div>
<div
className="upgrade-container"
style={{ paddingTop: '15px' }}
>
<a
id="green_upgrade"
className="btn btn-primary btn-block btn-lg"
href={upgradeUrl}
data-creative="sidebarupsell"
data-position="sidebar-message"
data-ol-has-click-handler=""
style={{ display: 'block', fontSize: '1em', fontWeight: 600 }}
>
Upgrade for <span className="price">{price}</span>
</a>
</div>
</div>
);
};
Sidecard.propTypes = {
options: PropTypes.shape({
state: PropTypes.shape({
upgradeDeadline: PropTypes.string.isRequired,
}),
access: PropTypes.shape({
accessExpirationDate: PropTypes.string.isRequired,
price: PropTypes.string.isRequired,
upgradeUrl: PropTypes.string.isRequired,
}),
}),
};
const futureDate = (numDays) => {
const defaultDate = new Date();
defaultDate.setDate(defaultDate.getDate() + numDays);
return defaultDate;
};
Sidecard.defaultProps = {
options: {
state: {
upgradeDeadline: new Date('Mar 29, 2021 11:59 PM EST'),
},
access: {
accessDeadline: futureDate(24),
price: '$23',
upgradeUrl: '',
},
},
};
export default Sidecard;

View File

@@ -1,234 +0,0 @@
import { useState } from 'react';
import { useModel } from '../../generic/model-store';
import MMP2PBlockModal from './BlockModal';
import MMP2PFlyover from './Flyover';
import MMP2PFlyoverMobile from './FlyoverMobile';
import MMP2PFlyoverTrigger from './FlyoverTrigger';
import MMP2PFlyoverTriggerMobile from './FlyoverTriggerMobile';
import MMP2PLockPaywall from './LockPaywall';
import MMP2PSidecard from './Sidecard';
import { isMobile, StrictDict } from './utils';
const MMP2PKeys = StrictDict({
enableFn: 'enable',
flyoverVisible: 'flyoverVisible',
state: 'state',
});
let location;
const windowKey = (field) => `experiment__mmp2p_${location}_${field}`;
const setWindowVal = (field, val) => {
window[windowKey(field)] = val;
};
const windowVal = (field) => window[windowKey(field)];
const defaultWindowVal = (field, val) => (
windowVal(field) === undefined ? val : windowVal(field)
);
const externalConfig = {
runs: [
{
upgradeDeadline: 'Mar 29 2021 11:59 PM EST',
courses: [
{
courseRun: 'course-v1:edX+DemoX+Demo_Course',
subSections: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e',
],
},
],
},
],
};
const initDatesMMP2P = () => {
location = 'dates';
const defaultState = {
isEnabled: false,
upgradeDeadline: null,
};
const [MMP2POptions, setMMP2POptions] = useState(
defaultWindowVal(MMP2PKeys.state, { ...defaultState }),
);
setWindowVal(MMP2PKeys.enableFn, (upgradeDeadline) => {
if (upgradeDeadline === undefined) {
setMMP2POptions({ ...defaultState });
} else {
setMMP2POptions({
isEnabled: true,
upgradeDeadline,
});
}
});
return {
state: MMP2POptions,
};
};
const initHomeMMP2P = (courseId) => {
location = 'home';
const defaultState = {
isEnabled: false,
upgradeDeadline: null,
afterUpgradeDeadline: false,
};
const [MMP2POptions, setMMP2POptions] = useState(
defaultWindowVal(MMP2PKeys.state, { ...defaultState }),
);
setWindowVal(MMP2PKeys.enableFn, (upgradeDeadline) => {
if (upgradeDeadline === undefined) {
setMMP2POptions({ ...defaultState });
} else {
setMMP2POptions({
isEnabled: true,
upgradeDeadline,
afterUpgradeDeadline: new Date() > new Date(upgradeDeadline),
});
}
});
const access = {
isAudit: false,
accessExpirationDate: null,
upgradeUrl: null,
price: null,
};
const { accessExpiration, verifiedMode } = useModel('outline', courseId);
if (accessExpiration !== null && accessExpiration !== undefined) {
access.isAudit = true;
access.accessExpirationDate = accessExpiration.expirationDate;
access.upgradeUrl = accessExpiration.upgradeUrl;
access.price = `${verifiedMode.currencySymbol}${verifiedMode.price}`;
}
return {
state: MMP2POptions,
access,
};
};
const initCoursewareMMP2P = (courseId, sequenceId, unitId) => {
location = 'course';
const defaultState = {
isEnabled: false,
upgradeDeadline: null,
afterUpgradeDeadline: false,
subSections: [],
isWhitelisted: false,
};
const [MMP2POptions, _setMMP2POptions] = useState(
defaultWindowVal(MMP2PKeys.state, { ...defaultState }),
);
const setMMP2POptions = (options) => {
_setMMP2POptions(options);
setWindowVal(MMP2PKeys.state, options);
};
const [isMMP2PFlyoverVisible, setMMP2PFlyoverVisible] = useState(
isMobile() ? false : defaultWindowVal(MMP2PKeys.flyoverVisible, false),
);
const flyover = {
isVisible: isMMP2PFlyoverVisible,
toggle: () => {
setMMP2PFlyoverVisible(!isMMP2PFlyoverVisible);
setWindowVal(MMP2PKeys.flyoverVisible, !isMMP2PFlyoverVisible);
},
};
setWindowVal(MMP2PKeys.enableFn,
(upgradeDeadline, subSections) => {
if (subSections.length !== undefined && subSections.length > 0) {
setMMP2POptions({
isEnabled: true,
upgradeDeadline,
afterUpgradeDeadline: new Date() > new Date(upgradeDeadline),
isWhitelisted: subSections.indexOf(sequenceId) > -1,
});
} else {
setMMP2POptions({ ...defaultState });
setWindowVal(MMP2PKeys.state, { ...defaultState });
}
});
const access = {
isAudit: false,
accessExpirationDate: null,
upgradeUrl: null,
price: null,
};
const { accessExpiration, verifiedMode } = useModel('coursewareMeta', courseId);
if (accessExpiration !== null && accessExpiration !== undefined) {
access.isAudit = true;
access.accessExpirationDate = accessExpiration.expirationDate;
access.upgradeUrl = accessExpiration.upgradeUrl;
access.price = `$${verifiedMode.price}`;
}
// testing
setWindowVal('externalConfig', externalConfig);
const unitModel = useModel('units', unitId);
const graded = unitModel !== undefined ? unitModel.graded : false;
const meta = {};
meta.verifiedLock = (
access.isAudit
&& !MMP2POptions.isWhitelisted
);
meta.gradedLock = (
access.isAudit
&& MMP2POptions.isWhitelisted
&& graded
);
meta.modalLock = (
access.isAudit
&& !MMP2POptions.isWhitelisted
&& MMP2POptions.afterUpgradeDeadline
);
meta.showLock = (
MMP2POptions.isEnabled
&& (meta.verifiedLock || meta.gradedLock)
);
meta.blockContent = (MMP2POptions.isEnabled && meta.verifiedLock);
return {
access,
flyover,
meta,
state: MMP2POptions,
};
};
export {
MMP2PBlockModal,
MMP2PFlyover,
MMP2PFlyoverMobile,
MMP2PFlyoverTrigger,
MMP2PFlyoverTriggerMobile,
MMP2PLockPaywall,
MMP2PSidecard,
initCoursewareMMP2P,
initHomeMMP2P,
initDatesMMP2P,
};

View File

@@ -1,6 +0,0 @@
@import "./BlockModal.scss";
@import "./Flyover.scss";
@import "./FlyoverMobile.scss";
@import "./FlyoverTrigger.scss";
@import "./FlyoverTriggerMobile.scss";
@import "./Sidecard.scss";

View File

@@ -1,48 +0,0 @@
/* eslint-disable no-console */
import { useContext } from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import util from 'util';
export const isMobile = () => {
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
return Boolean(
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i),
);
};
export const getUser = () => useContext(AppContext).authenticatedUser;
const staticReturnOptions = [
'dict',
'inspect',
Symbol.toStringTag,
util.inspect.custom,
Symbol.for('nodejs.util.inspect.custom'),
];
const strictGet = (target, name) => {
if (name === Symbol.toStringTag) {
return target;
}
if (name === 'length') {
return target.length;
}
if (staticReturnOptions.indexOf(name) >= 0) {
return target;
}
if (name === Symbol.iterator) {
return { ...target };
}
if (name in target || name === '_reactFragment') {
return target[name];
}
console.log(name.toString());
console.error({ target, name });
const e = Error(`invalid property "${name.toString()}"`);
console.error(e.stack);
return undefined;
};
export const StrictDict = (dict) => new Proxy(dict, { get: strictGet });

View File

@@ -369,6 +369,3 @@
@import 'course-home/outline-tab/widgets/UpgradeCard.scss';
@import 'course-home/outline-tab/widgets/ProctoringInfoPanel.scss';
@import 'courseware/course/course-exit/CourseRecommendationsExp/course_recommendations.exp';
/** [MM-P2P] Experiment */
@import 'experiments/mm-p2p/index.scss';