feat: remove mmp2p (#1042)

* feat: remove mmp2p experiment folder

* feat: remove AccessExpirationAlertMMP2P

* feat: remove imports references and conditionals for mmp2p
This commit is contained in:
Jansen Kantor
2023-01-19 15:35:01 -05:00
committed by GitHub
parent 5d477cebb2
commit b082f3ed19
36 changed files with 56 additions and 1690 deletions

View File

@@ -1,4 +1,3 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
@@ -8,18 +7,8 @@ import { Alert, Hyperlink } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import messages from './messages';
import AccessExpirationAlertMMP2P from './AccessExpirationAlertMMP2P';
function AccessExpirationAlert({ intl, payload }) {
/** [MM-P2P] Experiment */
const [showMMP2P, setShowMMP2P] = useState(!!window.experiment__home_alert_bShowMMP2P);
if (window.experiment__home_alert_showMMP2P === undefined) {
window.experiment__home_alert_showMMP2P = (val) => {
window.experiment__home_alert_bShowMMP2P = !!val;
setShowMMP2P(!!val);
};
}
const {
accessExpiration,
courseId,
@@ -39,13 +28,6 @@ function AccessExpirationAlert({ intl, payload }) {
upgradeUrl,
} = accessExpiration;
/** [MM-P2P] Experiment */
if (showMMP2P) {
return (
<AccessExpirationAlertMMP2P payload={payload} />
);
}
const logClick = () => {
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
org_key: org,

View File

@@ -1,80 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedDate, injectIntl } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import messages from './messages';
function AccessExpirationAlertMMP2P({ payload }) {
const {
accessExpiration,
userTimezone,
} = payload;
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
if (!accessExpiration) {
return null;
}
const {
expirationDate,
upgradeDeadline,
upgradeUrl,
} = accessExpiration;
let deadlineMessage = null;
const formatDate = (val, key) => (
<FormattedDate
key={`accessExpiration.${key}`}
day="numeric"
month="short"
year="numeric"
value={val}
{...timezoneFormatArgs}
/>
);
if (upgradeDeadline && upgradeUrl) {
deadlineMessage = (
<>
Upgrade by {formatDate(upgradeDeadline, 'upgradeDesc')} to unlock unlimited access to all course activities, including graded assignments.
&nbsp;
<Hyperlink
className="font-weight-bold"
style={{ textDecoration: 'underline' }}
destination={upgradeUrl}
>
{messages.upgradeNow.defaultMessage}
</Hyperlink>
</>
);
}
return (
<Alert variant="info" icon={Info}>
<span className="font-weight-bold">
Unlock full course content by {formatDate(upgradeDeadline, 'upgradeTitle')}
</span>
<br />
{deadlineMessage}
<br />
You lose all access to the first two weeks of scheduled content
on {formatDate(expirationDate, 'expirationBody')}.
</Alert>
);
}
AccessExpirationAlertMMP2P.propTypes = {
payload: PropTypes.shape({
accessExpiration: PropTypes.shape({
expirationDate: PropTypes.string.isRequired,
masqueradingExpiredCourse: PropTypes.bool.isRequired,
upgradeDeadline: PropTypes.string,
upgradeUrl: PropTypes.string,
}).isRequired,
userTimezone: PropTypes.string.isRequired,
}).isRequired,
};
export default injectIntl(AccessExpirationAlertMMP2P);

View File

@@ -9,8 +9,6 @@ import Timeline from './timeline/Timeline';
import { fetchDatesTab } from '../data';
import { useModel } from '../../generic/model-store';
/** [MM-P2P] Experiment */
import { initDatesMMP2P } from '../../experiments/mm-p2p';
import SuggestedScheduleHeader from '../suggested-schedule-messaging/SuggestedScheduleHeader';
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
@@ -30,9 +28,6 @@ function DatesTab({ intl }) {
courseDateBlocks,
} = useModel('dates', courseId);
/** [MM-P2P] Experiment */
const mmp2p = initDatesMMP2P(courseId);
const hasDeadlines = courseDateBlocks && courseDateBlocks.some(x => x.dateType === 'assignment-due-date');
const logUpgradeLinkClick = () => {
@@ -51,8 +46,7 @@ function DatesTab({ intl }) {
<div role="heading" aria-level="1" className="h2 my-3">
{intl.formatMessage(messages.title)}
</div>
{ /** [MM-P2P] Experiment */ }
{isSelfPaced && hasDeadlines && !mmp2p.state.isEnabled && (
{isSelfPaced && hasDeadlines && (
<>
<ShiftDatesAlert model="dates" fetch={fetchDatesTab} />
<SuggestedScheduleHeader />
@@ -60,7 +54,7 @@ function DatesTab({ intl }) {
<UpgradeToShiftDatesAlert logUpgradeLinkClick={logUpgradeLinkClick} model="dates" />
</>
)}
<Timeline mmp2p={mmp2p} />
<Timeline />
</>
);
}

View File

@@ -23,8 +23,6 @@ function Day({
intl,
items,
last,
/** [MM-P2P] Example */
mmp2p,
}) {
const {
courseId,
@@ -37,11 +35,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 */}
@@ -57,8 +50,7 @@ function Day({
<div className="d-inline-block ml-3 pl-2">
<div className="row w-100 m-0 mb-1 align-items-center text-primary-700" data-testid="dates-header">
<FormattedDate
/** [MM-P2P] Experiment */
value={mmp2pOverride ? mmp2p.state.upgradeDeadline : date}
value={date}
day="numeric"
month="short"
weekday="short"
@@ -68,10 +60,7 @@ 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 showDueDateTime = item.dateType === 'assignment-due-date';
const showLink = item.link && isLearnerAssignment(item);
@@ -107,15 +96,7 @@ function Day({
</OverlayTrigger>
)}
</div>
{ /** [MM-P2P] Experiment (conditional) */ }
{ mmp2pOverride
? (
<div className="small mb-2">
You are still eligible 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>
);
})}
@@ -138,25 +119,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 mt-4 pt-2">
{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

@@ -12,8 +12,6 @@ import './DateSummary.scss';
export default function DateSummary({
dateBlock,
userTimezone,
/** [MM-P2P] Experiment */
mmp2p,
}) {
const {
courseId,
@@ -25,9 +23,6 @@ export default function DateSummary({
const linkedTitle = dateBlock.link && isLearnerAssignment(dateBlock);
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
/** [MM-P2P] Experiment */
const showMMP2P = mmp2p.state.isEnabled && (dateBlock.dateType === 'verified-upgrade-deadline');
const logVerifiedUpgradeClick = () => {
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
org_key: org,
@@ -45,8 +40,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"
@@ -55,45 +49,30 @@ export default function DateSummary({
/>
</div>
</div>
{/** [MM-P2P] Experiment (conditional) */}
{ showMMP2P ? (
<div className="row ml-4 pr-2">
<div className="date-summary-text">
<div className="row ml-4 pr-2">
<div className="date-summary-text">
{linkedTitle && (
<div className="font-weight-bold mt-2">
Last chance to upgrade
<a href={dateBlock.link}>{dateBlock.title}</a>
</div>
</div>
<div className="date-summary-text mt-1">
You are still eligible to upgrade to a Verified Certificate!
&nbsp; Unlock full course access and highlight the knowledge you&apos;ll gain.
</div>
</div>
) : (
<div className="row ml-4 pr-2">
<div className="date-summary-text">
{linkedTitle && (
<div className="font-weight-bold mt-2">
<a href={dateBlock.link}>{dateBlock.title}</a>
</div>
)}
{!linkedTitle && (
<div className="font-weight-bold mt-2">{dateBlock.title}</div>
)}
</div>
{dateBlock.description && (
<div className="date-summary-text mt-1">{dateBlock.description}</div>
)}
{!linkedTitle && dateBlock.link && (
<a
href={dateBlock.link}
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
className="description-link"
>
{dateBlock.linkText}
</a>
{!linkedTitle && (
<div className="font-weight-bold mt-2">{dateBlock.title}</div>
)}
</div>
)}
{dateBlock.description && (
<div className="date-summary-text mt-1">{dateBlock.description}</div>
)}
{!linkedTitle && dateBlock.link && (
<a
href={dateBlock.link}
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
className="description-link"
>
{dateBlock.linkText}
</a>
)}
</div>
</li>
);
}
@@ -109,22 +88,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

@@ -29,9 +29,6 @@ import WelcomeMessage from './widgets/WelcomeMessage';
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
/** [MM-P2P] Experiment */
import { initHomeMMP2P, MMP2PFlyover } from '../../experiments/mm-p2p';
function OutlineTab({ intl }) {
const {
courseId,
@@ -104,9 +101,6 @@ function OutlineTab({ intl }) {
return userRoleNames.includes('enterprise_learner');
};
/** [[MM-P2P] Experiment */
const MMP2P = initHomeMMP2P(courseId);
/** show post enrolment survey to only B2C learners */
const learnerType = isEnterpriseUser() ? 'enterprise_learner' : 'b2c_learner';
@@ -134,7 +128,6 @@ function OutlineTab({ intl }) {
<div role="heading" aria-level="1" className="h2">{title}</div>
</div>
</div>
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
<div className="row course-outline-tab">
<AccountActivationAlert />
<div className="col-12">
@@ -146,21 +139,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={{
...certificateAvailableAlert,
...courseEndAlert,
...courseStartAlert,
...scheduledContentAlert,
}}
/>
)}
{isSelfPaced && hasDeadlines && !MMP2P.state.isEnabled && (
<AlertList
topic="outline-course-alerts"
className="mb-3"
customAlerts={{
...certificateAvailableAlert,
...courseEndAlert,
...courseStartAlert,
...scheduledContentAlert,
}}
/>
{isSelfPaced && hasDeadlines && (
<>
<ShiftDatesAlert model="outline" fetch={fetchOutlineTab} />
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
@@ -203,28 +192,20 @@ function OutlineTab({ intl }) {
/>
)}
<CourseTools />
{ /** [MM-P2P] Experiment (conditional) */ }
{ MMP2P.state.isEnabled
? <MMP2PFlyover isStatic options={MMP2P} />
: (
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
/>
)}
<CourseDates
/** [MM-P2P] Experiment */
mmp2p={MMP2P}
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
/>
<CourseDates />
<CourseHandouts />
</div>
)}

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -10,8 +9,6 @@ import { useModel } from '../../../generic/model-store';
function CourseDates({
intl,
/** [MM-P2P] Experiment */
mmp2p,
}) {
const {
courseId,
@@ -40,8 +37,6 @@ function CourseDates({
key={courseDateBlock.title + courseDateBlock.date}
dateBlock={courseDateBlock}
userTimezone={userTimezone}
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
/>
))}
</ol>
@@ -55,13 +50,6 @@ function CourseDates({
CourseDates.propTypes = {
intl: intlShape.isRequired,
/** [MM-P2P] Experiment */
mmp2p: PropTypes.shape({}),
};
CourseDates.defaultProps = {
/** [MM-P2P] Experiment */
mmp2p: {},
};
export default injectIntl(CourseDates);

View File

@@ -18,9 +18,6 @@ import SidebarTriggers from './sidebar/SidebarTriggers';
import { useModel } from '../../generic/model-store';
import { getSessionStorage, setSessionStorage } from '../../data/sessionStorage';
/** [MM-P2P] Experiment */
import { initCoursewareMMP2P, MMP2PBlockModal } from '../../experiments/mm-p2p';
function Course({
courseId,
sequenceId,
@@ -71,9 +68,6 @@ function Course({
}
}
/** [MM-P2P] Experiment */
const MMP2P = initCoursewareMMP2P(courseId, sequenceId, unitId);
return (
<SidebarProvider courseId={courseId} unitId={unitId}>
<Helmet>
@@ -86,8 +80,6 @@ function Course({
sequenceId={sequenceId}
isStaff={isStaff}
unitId={unitId}
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}
/>
{shouldDisplayTriggers && (
<SidebarTriggers />
@@ -102,8 +94,6 @@ function Course({
unitNavigationHandler={unitNavigationHandler}
nextSequenceHandler={nextSequenceHandler}
previousSequenceHandler={previousSequenceHandler}
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}
/>
<CelebrationModal
courseId={courseId}
@@ -117,8 +107,6 @@ function Course({
onClose={() => setWeeklyGoalCelebrationOpen(false)}
/>
<ContentTools course={course} />
{ /** [MM-P2P] Experiment */ }
{ MMP2P.meta.modalLock && <MMP2PBlockModal options={MMP2P} /> }
</SidebarProvider>
);
}

View File

@@ -8,8 +8,6 @@ import { useSelector } from 'react-redux';
import { SelectMenu } from '@edx/paragon';
import { Link } from 'react-router-dom';
import { useModel, useModels } from '../../generic/model-store';
/** [MM-P2P] Experiment */
import { MMP2PFlyoverTrigger } from '../../experiments/mm-p2p';
import JumpNavMenuItem from './JumpNavMenuItem';
function CourseBreadcrumb({
@@ -87,8 +85,6 @@ export default function CourseBreadcrumbs({
sequenceId,
unitId,
isStaff,
/** [MM-P2P] Experiment */
mmp2p,
}) {
const course = useModel('coursewareMeta', courseId);
const courseStatus = useSelector(state => state.courseware.courseStatus);
@@ -152,10 +148,6 @@ export default function CourseBreadcrumbs({
isStaff={isStaff}
/>
))}
{/** [MM-P2P] Experiment */}
{mmp2p.state && mmp2p.state.isEnabled && (
<MMP2PFlyoverTrigger options={mmp2p} />
)}
</ol>
</nav>
);
@@ -167,12 +159,6 @@ CourseBreadcrumbs.propTypes = {
sequenceId: PropTypes.string,
unitId: PropTypes.string,
isStaff: PropTypes.bool,
/** [MM-P2P] Experiment */
mmp2p: PropTypes.shape({
state: PropTypes.shape({
isEnabled: PropTypes.bool.isRequired,
}),
}),
};
CourseBreadcrumbs.defaultProps = {
@@ -180,6 +166,4 @@ CourseBreadcrumbs.defaultProps = {
sequenceId: null,
unitId: null,
isStaff: null,
/** [MM-P2P] Experiment */
mmp2p: {},
};

View File

@@ -26,10 +26,6 @@ import HiddenAfterDue from './hidden-after-due';
import { SequenceNavigation, UnitNavigation } from './sequence-navigation';
import SequenceContent from './SequenceContent';
/** [MM-P2P] Experiment */
import { isMobile } from '../../../experiments/mm-p2p/utils';
import { MMP2PFlyover, MMP2PFlyoverMobile } from '../../../experiments/mm-p2p';
function Sequence({
unitId,
sequenceId,
@@ -38,7 +34,6 @@ function Sequence({
nextSequenceHandler,
previousSequenceHandler,
intl,
mmp2p,
}) {
const course = useModel('coursewareMeta', courseId);
const {
@@ -155,8 +150,6 @@ function Sequence({
sequenceId={sequenceId}
unitId={unitId}
className="mb-4"
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
nextSequenceHandler={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'top');
handleNext();
@@ -180,8 +173,6 @@ function Sequence({
sequenceId={sequenceId}
unitId={unitId}
unitLoadedHandler={handleUnitLoaded}
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
/>
{unitHasLoaded && (
<UnitNavigation
@@ -201,13 +192,6 @@ function Sequence({
</div>
</div>
<Sidebar />
{/** [MM-P2P] Experiment */}
{(mmp2p.state.isEnabled && mmp2p.flyover.isVisible) && (
isMobile()
? <MMP2PFlyoverMobile options={mmp2p} />
: <MMP2PFlyover options={mmp2p} />
)}
</div>
);
@@ -244,30 +228,11 @@ Sequence.propTypes = {
nextSequenceHandler: PropTypes.func.isRequired,
previousSequenceHandler: 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,
}),
}),
};
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

@@ -16,8 +16,6 @@ function SequenceContent({
sequenceId,
unitId,
unitLoadedHandler,
/** [MM-P2P] Experiment */
mmp2p,
}) {
const sequence = useModel('sequences', sequenceId);
@@ -61,8 +59,6 @@ function SequenceContent({
key={unitId}
id={unitId}
onLoaded={unitLoadedHandler}
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
/>
);
}
@@ -74,28 +70,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

@@ -8,8 +8,6 @@ import React, {
} from 'react';
import { useDispatch } from 'react-redux';
import { processEvent } from '../../../course-home/data/thunks';
/** [MM-P2P] Experiment */
import { MMP2PLockPaywall } from '../../../experiments/mm-p2p';
import { useEventListener } from '../../../generic/hooks';
import { useModel } from '../../../generic/model-store';
import PageLoading from '../../../generic/PageLoading';
@@ -82,8 +80,6 @@ function Unit({
onLoaded,
id,
intl,
/** [MM-P2P] Experiment */
mmp2p,
}) {
const { authenticatedUser } = useContext(AppContext);
const view = authenticatedUser ? 'student_view' : 'public_view';
@@ -155,7 +151,7 @@ function Unit({
/>
{/* TODO: social share exp. Need to remove later */}
{window.expSocialShareEnabled && <ShareButton url={window.expSocialShareAboutUrl} />}
{ !mmp2p.state.isEnabled && contentTypeGatingEnabled && unit.containsContentTypeGatedContent && (
{contentTypeGatingEnabled && unit.containsContentTypeGatedContent && (
<Suspense
fallback={(
<PageLoading
@@ -166,11 +162,7 @@ function Unit({
<LockPaywall courseId={courseId} />
</Suspense>
)}
{ /** [MM-P2P] Experiment */ }
{ mmp2p.meta.showLock && (
<MMP2PLockPaywall options={mmp2p} />
)}
{!mmp2p.meta.blockContent && shouldDisplayHonorCode && (
{shouldDisplayHonorCode && (
<Suspense
fallback={(
<PageLoading
@@ -181,13 +173,12 @@ function Unit({
<HonorCode courseId={courseId} />
</Suspense>
)}
{ /** [MM-P2P] Experiment (conditional) */ }
{!mmp2p.meta.blockContent && !shouldDisplayHonorCode && !hasLoaded && !showError && (
{!shouldDisplayHonorCode && !hasLoaded && !showError && (
<PageLoading
srMessage={intl.formatMessage(messages.loadingSequence)}
/>
)}
{!mmp2p.meta.blockContent && !shouldDisplayHonorCode && !hasLoaded && showError && (
{!shouldDisplayHonorCode && !hasLoaded && showError && (
<ErrorPage />
)}
{modalOptions.open && (
@@ -215,8 +206,7 @@ function Unit({
dialogClassName="modal-lti"
/>
)}
{ /** [MM-P2P] Experiment (conditional) */ }
{ !mmp2p.meta.blockContent && !shouldDisplayHonorCode && (
{!shouldDisplayHonorCode && (
<div className="unit-iframe-wrapper">
<iframe
id="unit-iframe"
@@ -255,31 +245,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

@@ -19,8 +19,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,
@@ -31,7 +29,6 @@ function SequenceNavigation({
nextSequenceHandler,
previousSequenceHandler,
goToCourseExitPage,
mmp2p,
}) {
const sequence = useModel('sequences', sequenceId);
const { isFirstUnit, isLastUnit } = useSequenceNavigationMetadata(sequenceId, unitId);
@@ -90,8 +87,6 @@ function SequenceNavigation({
{renderUnitButtons()}
{renderNextButton()}
{/** [MM-P2P] Experiment */}
{ mmp2p.state.isEnabled && <MMP2PFlyoverTriggerMobile options={mmp2p} /> }
</nav>
);
}
@@ -105,22 +100,11 @@ SequenceNavigation.propTypes = {
nextSequenceHandler: PropTypes.func.isRequired,
previousSequenceHandler: PropTypes.func.isRequired,
goToCourseExitPage: PropTypes.func.isRequired,
/** [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 resumé
</BulletList>
<BulletList>
Unlock unlimited access to all course content and activities,
&nbsp;including graded assignments, even after the course ends.
</BulletList>
<BulletList>
Support our 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,173 +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 resumé
</BulletList>
<BulletList>
Support our <span style={{ fontWeight: 600 }}>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,272 +0,0 @@
import { useState } from 'react';
import { useDispatch } from 'react-redux';
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',
access: 'access',
meta: 'meta',
});
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 createWindowStateSetter = (stateSetter, key) => (value) => {
stateSetter(value);
setWindowVal(key, value);
};
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 defaultAccess = {
isAudit: false,
accessExpirationDate: null,
upgradeUrl: null,
price: null,
};
const [MMP2POptions, _setMMP2POptions] = useState(defaultWindowVal(MMP2PKeys.state, { ...defaultState }));
const [MMP2PAccess, _setMMP2PAccess] = useState(defaultWindowVal(MMP2PKeys.access, { ...defaultAccess }));
const setMMP2POptions = createWindowStateSetter(_setMMP2POptions, MMP2PKeys.state);
const setMMP2PAccess = createWindowStateSetter(_setMMP2PAccess, MMP2PKeys.access);
const { accessExpiration, verifiedMode } = useModel('outline', courseId);
const loadAccess = () => {
if (accessExpiration !== null && accessExpiration !== undefined) {
setMMP2PAccess({
isAudit: true,
accessExpirationDate: accessExpiration.expirationDate,
upgradeUrl: accessExpiration.upgradeUrl,
price: ((verifiedMode !== null && verifiedMode !== undefined)
? `${verifiedMode.currencySymbol}${verifiedMode.price}`
: ''
),
});
}
};
const enableFunction = (upgradeDeadline) => {
if (upgradeDeadline === undefined) {
setMMP2POptions({ ...defaultState });
setMMP2PAccess({ ...defaultAccess });
} else {
setMMP2POptions({
isEnabled: true,
upgradeDeadline,
afterUpgradeDeadline: new Date() > new Date(upgradeDeadline),
});
loadAccess();
}
};
setWindowVal(MMP2PKeys.enableFn, enableFunction);
return {
state: MMP2POptions,
access: MMP2PAccess,
};
};
const initCoursewareMMP2P = (courseId, sequenceId, unitId) => {
location = 'course';
const defaultState = {
isEnabled: false,
upgradeDeadline: null,
afterUpgradeDeadline: false,
subSections: [],
isWhitelisted: false,
};
const defaultAccess = {
isAudit: false,
accessExpirationDate: null,
upgradeUrl: null,
price: null,
};
const defaultMeta = {
blockContent: false,
gradedLock: false,
modalLock: false,
showLock: false,
verifiedLock: false,
};
const [MMP2POptions, _setMMP2POptions] = useState(defaultWindowVal(MMP2PKeys.state, { ...defaultState }));
const [MMP2PAccess, _setMMP2PAccess] = useState(defaultWindowVal(MMP2PKeys.access, { ...defaultAccess }));
const [MMP2PMeta, _setMMP2PMeta] = useState(defaultWindowVal(MMP2PKeys.meta, { ...defaultMeta }));
const [MMP2PIsFlyoverVisible, setMMP2PIsFlyoverVisible] = useState(
defaultWindowVal(MMP2PKeys.flyoverVisible, !isMobile()),
);
const setMMP2POptions = createWindowStateSetter(_setMMP2POptions, MMP2PKeys.state);
const setMMP2PAccess = createWindowStateSetter(_setMMP2PAccess, MMP2PKeys.access);
const setMMP2PMeta = createWindowStateSetter(_setMMP2PMeta, MMP2PKeys.meta);
const flyover = {
isVisible: MMP2PIsFlyoverVisible,
toggle: () => {
setMMP2PIsFlyoverVisible(!MMP2PIsFlyoverVisible);
setWindowVal(MMP2PKeys.flyoverVisible, !MMP2PIsFlyoverVisible);
},
};
const loadOptions = (upgradeDeadline, subSections) => (dispatch, getState) => {
const state = getState();
const options = {
isEnabled: true,
upgradeDeadline,
afterUpgradeDeadline: new Date() > new Date(upgradeDeadline),
isWhitelisted: subSections.indexOf(sequenceId) > -1,
};
setMMP2POptions(options);
const models = {
coursewareMeta: state.models.coursewareMeta[courseId],
courseHomeMeta: state.models.courseHomeMeta[courseId],
units: state.models.units[unitId],
};
const { accessExpiration } = models.coursewareMeta;
const { verifiedMode } = models.courseHomeMeta;
const graded = models.units !== undefined ? models.units.graded : false;
let access = {};
if (accessExpiration !== null && accessExpiration !== undefined) {
access = {
isAudit: true,
accessExpirationDate: accessExpiration.expirationDate,
upgradeUrl: accessExpiration.upgradeUrl,
price: ((verifiedMode !== null && verifiedMode !== undefined)
? `${verifiedMode.currencySymbol}${verifiedMode.price}`
: ''
),
};
setMMP2PAccess(access);
}
const meta = {
verifiedLock: (access.isAudit && !options.isWhitelisted),
gradedLock: (access.isAudit && options.isWhitelisted && graded),
modalLock: (access.isAudit && !options.isWhitelisted && options.afterUpgradeDeadline),
};
meta.showLock = (options.isEnabled && (meta.verifiedLock || meta.gradedLock));
meta.blockContent = (options.isEnabled && meta.verifiedLock);
setMMP2PMeta(meta);
};
const dispatch = useDispatch();
const enableFunction = (upgradeDeadline, subSections) => {
if (subSections.length !== undefined && subSections.length > 0) {
dispatch(loadOptions(upgradeDeadline, subSections));
} else {
setMMP2POptions({ ...defaultState });
setMMP2PAccess({ ...defaultAccess });
setMMP2PMeta({ ...defaultMeta });
}
};
setWindowVal(MMP2PKeys.enableFn, enableFunction);
// testing
setWindowVal('externalConfig', externalConfig);
const config = {
access: MMP2PAccess,
flyover,
meta: MMP2PMeta,
state: MMP2POptions,
};
return config;
};
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

@@ -377,6 +377,3 @@
@import "course-home/progress-tab/grades/course-grade/GradeBar.scss";
@import "courseware/course/course-exit/CourseRecommendations";
@import "product-tours/newUserCourseHomeTour/NewUserCourseHomeTourModal.scss";
/** [MM-P2P] Experiment */
@import "experiments/mm-p2p/index.scss";