Compare commits
6 Commits
bw/hackath
...
bjh/accura
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82e26623ac | ||
|
|
8edebbfd85 | ||
|
|
250b118c50 | ||
|
|
e86f7b7b60 | ||
|
|
152931636d | ||
|
|
1f7b875ea6 |
@@ -429,6 +429,7 @@ Object {
|
|||||||
"hasVisitedCourse": false,
|
"hasVisitedCourse": false,
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
||||||
},
|
},
|
||||||
|
"timeOffsetMillis": 0,
|
||||||
"verifiedMode": Object {
|
"verifiedMode": Object {
|
||||||
"accessExpirationDate": "2050-01-01T12:00:00",
|
"accessExpirationDate": "2050-01-01T12:00:00",
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
|
|||||||
@@ -157,8 +157,12 @@ export async function getProctoringInfoData(courseId) {
|
|||||||
export async function getOutlineTabData(courseId) {
|
export async function getOutlineTabData(courseId) {
|
||||||
const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`;
|
const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`;
|
||||||
let { tabData } = {};
|
let { tabData } = {};
|
||||||
|
let requestTime = Date.now();
|
||||||
|
let responseTime = requestTime;
|
||||||
try {
|
try {
|
||||||
|
requestTime = Date.now();
|
||||||
tabData = await getAuthenticatedHttpClient().get(url);
|
tabData = await getAuthenticatedHttpClient().get(url);
|
||||||
|
responseTime = Date.now();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { httpErrorStatus } = error && error.customAttributes;
|
const { httpErrorStatus } = error && error.customAttributes;
|
||||||
if (httpErrorStatus === 404) {
|
if (httpErrorStatus === 404) {
|
||||||
@@ -168,6 +172,18 @@ export async function getOutlineTabData(courseId) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Time offset computation should move down into the HttpClient wrapper to maintain a global time correction reference
|
||||||
|
// Requires 'Access-Control-Expose-Headers: Date' on the server response per https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-expose-headers
|
||||||
|
const headerDate = tabData.headers.date;
|
||||||
|
|
||||||
|
let timeOffsetMillis = 0;
|
||||||
|
if (headerDate !== undefined) {
|
||||||
|
const headerTime = Date.parse(headerDate);
|
||||||
|
const roundTripMillis = requestTime - responseTime;
|
||||||
|
const localTime = responseTime - (roundTripMillis / 2); // Roughly compensate for transit time
|
||||||
|
timeOffsetMillis = headerTime - localTime;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
} = tabData;
|
} = tabData;
|
||||||
@@ -187,6 +203,7 @@ export async function getOutlineTabData(courseId) {
|
|||||||
const welcomeMessageHtml = data.welcome_message_html;
|
const welcomeMessageHtml = data.welcome_message_html;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
timeOffsetMillis, // This should move to a global time correction reference
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
canShowUpgradeSock,
|
canShowUpgradeSock,
|
||||||
courseBlocks,
|
courseBlocks,
|
||||||
|
|||||||
@@ -1,22 +1,241 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Icon } from '@edx/paragon';
|
||||||
|
import { Check } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedDate, FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@edx/paragon';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
import messages from '../messages';
|
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import { UpgradeButton } from '../../../generic/upgrade-button';
|
import { UpgradeButton } from '../../../generic/upgrade-button';
|
||||||
import VerifiedCert from '../../../generic/assets/edX_certificate.png';
|
|
||||||
|
|
||||||
function UpgradeCard({ courseId, intl, onLearnMore }) {
|
function UpsellNoFBECardContent() {
|
||||||
|
return (
|
||||||
|
<ul className="fa-ul">
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.verifiedCertLink"
|
||||||
|
defaultMessage="Earn a {verifiedCertLink} of completion to showcase on your resume"
|
||||||
|
values={{
|
||||||
|
verifiedCertLink: (
|
||||||
|
<a className="inline-link-underline" rel="noopener noreferrer" target="_blank" href={`${getConfig().MARKETING_SITE_BASE_URL}/verified-certificate`}>verified certificate</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.nonProfitMission"
|
||||||
|
defaultMessage="Support our {nonProfitMission} at edX"
|
||||||
|
values={{
|
||||||
|
nonProfitMission: (
|
||||||
|
<span className="font-weight-bold">non-profit mission</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpsellFBEFarAwayCardContent() {
|
||||||
|
return (
|
||||||
|
<ul className="fa-ul">
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.verifiedCertLink"
|
||||||
|
defaultMessage="Earn a {verifiedCertLink} of completion to showcase on your resume"
|
||||||
|
values={{
|
||||||
|
verifiedCertLink: (
|
||||||
|
<a className="inline-link-underline" rel="noopener noreferrer" target="_blank" href={`${getConfig().MARKETING_SITE_BASE_URL}/verified-certificate`}>verified certificate</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.unlock-graded"
|
||||||
|
defaultMessage="Unlock your access to all course activities, including {gradedAssignments}"
|
||||||
|
values={{
|
||||||
|
gradedAssignments: (
|
||||||
|
<span className="font-weight-bold">graded assignments</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.fullAccess"
|
||||||
|
defaultMessage="{fullAccess} to course content and materials, even after the course ends"
|
||||||
|
values={{
|
||||||
|
fullAccess: (
|
||||||
|
<span className="font-weight-bold">Full access</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.nonProfitMission"
|
||||||
|
defaultMessage="Support our {nonProfitMission} at edX"
|
||||||
|
values={{
|
||||||
|
nonProfitMission: (
|
||||||
|
<span className="font-weight-bold">non-profit mission</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpsellFBESoonCardContent({ accessExpirationDate, timezoneFormatArgs }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.expirationAccessLoss"
|
||||||
|
defaultMessage="You will lose all access to this course, {includingAnyProgress}, on {date}."
|
||||||
|
values={{
|
||||||
|
includingAnyProgress: (<span className="font-weight-bold">including any progress</span>),
|
||||||
|
date: (
|
||||||
|
<FormattedDate
|
||||||
|
key="accessDate"
|
||||||
|
day="numeric"
|
||||||
|
month="long"
|
||||||
|
value={new Date(accessExpirationDate)}
|
||||||
|
{...timezoneFormatArgs}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.expirationVerifiedCert"
|
||||||
|
defaultMessage="Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}."
|
||||||
|
values={{
|
||||||
|
benefitsOfUpgrading: (<a className="inline-link-underline" rel="noopener noreferrer" target="_blank" href="https://support.edx.org/hc/en-us/articles/360013426573-What-are-the-differences-between-audit-free-and-verified-paid-courses-">benefits of upgrading</a>),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsellFBESoonCardContent.propTypes = {
|
||||||
|
accessExpirationDate: PropTypes.PropTypes.instanceOf(Date).isRequired,
|
||||||
|
timezoneFormatArgs: PropTypes.shape({
|
||||||
|
timeZone: PropTypes.string,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
UpsellFBESoonCardContent.defaultProps = {
|
||||||
|
timezoneFormatArgs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function ExpirationCountdown({ hoursToExpiration }) {
|
||||||
|
let expirationText;
|
||||||
|
|
||||||
|
if (hoursToExpiration >= 24) {
|
||||||
|
expirationText = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.expiration.days"
|
||||||
|
defaultMessage={`{dayCount, number} {dayCount, plural,
|
||||||
|
one {day}
|
||||||
|
other {days}} left`}
|
||||||
|
values={{
|
||||||
|
dayCount: (Math.floor(hoursToExpiration / 24)),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (hoursToExpiration >= 1) {
|
||||||
|
expirationText = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.expiration.hours"
|
||||||
|
defaultMessage={`{hourCount, number} {hourCount, plural,
|
||||||
|
one {hour}
|
||||||
|
other {hours}} left`}
|
||||||
|
values={{
|
||||||
|
hourCount: (hoursToExpiration),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expirationText = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.expiration.minutes"
|
||||||
|
defaultMessage="Less than 1 hour left"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (<div className="p-3 upsell-warning">{expirationText}</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpirationCountdown.propTypes = {
|
||||||
|
hoursToExpiration: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function AccessExpirationDateBanner({ accessExpirationDate, timezoneFormatArgs }) {
|
||||||
|
return (
|
||||||
|
<div className="p-3 upsell-warning-light">
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.expirationr"
|
||||||
|
defaultMessage="Course access will expire {date}"
|
||||||
|
values={{
|
||||||
|
date: (
|
||||||
|
<FormattedDate
|
||||||
|
key="accessExpireDate"
|
||||||
|
day="numeric"
|
||||||
|
month="long"
|
||||||
|
value={accessExpirationDate}
|
||||||
|
{...timezoneFormatArgs}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessExpirationDateBanner.propTypes = {
|
||||||
|
accessExpirationDate: PropTypes.PropTypes.instanceOf(Date).isRequired,
|
||||||
|
timezoneFormatArgs: PropTypes.shape({
|
||||||
|
timeZone: PropTypes.string,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
AccessExpirationDateBanner.defaultProps = {
|
||||||
|
timezoneFormatArgs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function UpgradeCard({ courseId }) {
|
||||||
const { org } = useModel('courseHomeMeta', courseId);
|
const { org } = useModel('courseHomeMeta', courseId);
|
||||||
const {
|
const {
|
||||||
offer,
|
offer,
|
||||||
verifiedMode,
|
verifiedMode,
|
||||||
|
accessExpiration,
|
||||||
|
datesBannerInfo: {
|
||||||
|
contentTypeGatingEnabled,
|
||||||
|
},
|
||||||
|
datesWidget: {
|
||||||
|
userTimezone,
|
||||||
|
},
|
||||||
|
timeOffsetMillis,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
const correctedTime = new Date(Date.now() + timeOffsetMillis);
|
||||||
|
|
||||||
if (!verifiedMode) {
|
if (!verifiedMode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -34,6 +253,28 @@ function UpgradeCard({ courseId, intl, onLearnMore }) {
|
|||||||
...eventProperties,
|
...eventProperties,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function expirationHighlight(hoursToExpiration){
|
||||||
|
let expirationText;
|
||||||
|
if(hoursToExpiration < 24){
|
||||||
|
expirationText= <FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.expiration"
|
||||||
|
defaultMessage="{expiration} hours left"
|
||||||
|
values={{
|
||||||
|
expiration: (hoursToDiscountExpiration),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
expirationText =<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.expiration"
|
||||||
|
defaultMessage="{expiration} days left"
|
||||||
|
values={{
|
||||||
|
expiration: (Math.floor(hoursToExpiration/24)),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
return(<div className="p-3 upsell-warning">{expirationText}</div>)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendTrackingLogEvent('edx.bi.course.upgrade.sidebarupsell.displayed', eventProperties);
|
sendTrackingLogEvent('edx.bi.course.upgrade.sidebarupsell.displayed', eventProperties);
|
||||||
sendTrackEvent('Promotion Viewed', promotionEventProperties);
|
sendTrackEvent('Promotion Viewed', promotionEventProperties);
|
||||||
@@ -55,53 +296,110 @@ function UpgradeCard({ courseId, intl, onLearnMore }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
/*
|
||||||
<section className="mb-4 p-3 outline-sidebar-upgrade-card">
|
There are 4 parts that change in the upgrade card:
|
||||||
<h2 className="h4" id="outline-sidebar-upgrade-header">{intl.formatMessage(messages.upgradeTitle)}</h2>
|
upgradeCardHeaderText
|
||||||
<div className="row w-100 m-0">
|
expirationBanner
|
||||||
<div className="col-6 col-md-12 col-lg-3 col-xl-4 p-0 text-md-center text-lg-left">
|
upsellMessage
|
||||||
<img
|
offerCode
|
||||||
alt={intl.formatMessage(messages.certAlt)}
|
*/
|
||||||
className="w-100"
|
let upgradeCardHeaderText;
|
||||||
src={VerifiedCert}
|
let expirationBanner;
|
||||||
style={{ maxWidth: '10rem' }}
|
let upsellMessage;
|
||||||
|
let offerCode;
|
||||||
|
|
||||||
|
if (!!accessExpiration && !!contentTypeGatingEnabled) {
|
||||||
|
if (offer) { // if there's a first purchase discount, show it
|
||||||
|
const hoursToDiscountExpiration = Math.floor((new Date(offer.expirationDate) - correctedTime) / 1000 / 60 / 60);
|
||||||
|
|
||||||
|
upgradeCardHeaderText = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.firstTimeLearnerDiscount"
|
||||||
|
defaultMessage="{percentage}% First-Time Learner Discount"
|
||||||
|
values={{
|
||||||
|
percentage: (offer.percentage),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expirationBanner = <ExpirationCountdown hoursToExpiration={hoursToDiscountExpiration} />;
|
||||||
|
upsellMessage = <UpsellFBEFarAwayCardContent />;
|
||||||
|
offerCode = (
|
||||||
|
<div className="bg-light p-3 text-center discount-info">
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.code"
|
||||||
|
defaultMessage="Use code {code} at checkout"
|
||||||
|
values={{
|
||||||
|
code: (<span className="font-weight-bold">{offer.code}</span>),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6 col-md-12 col-lg-9 col-xl-8 p-0 pl-lg-2 text-center mt-md-2 mt-lg-0">
|
);
|
||||||
<div className="row w-100 m-0 justify-content-center">
|
} else {
|
||||||
<UpgradeButton
|
const accessExpirationDate = new Date(accessExpiration.expirationDate);
|
||||||
offer={offer}
|
const hoursToAccessExpiration = Math.floor((accessExpirationDate - correctedTime) / 1000 / 60 / 60);
|
||||||
onClick={logClick}
|
|
||||||
verifiedMode={verifiedMode}
|
if (hoursToAccessExpiration >= (7 * 24)) {
|
||||||
/>
|
upgradeCardHeaderText = (
|
||||||
{onLearnMore && (
|
<FormattedMessage
|
||||||
<div className="col-12">
|
id="learning.outline.alert.upgradecard.accessExpiration"
|
||||||
<Button
|
defaultMessage="Upgrade your course today"
|
||||||
variant="link"
|
/>
|
||||||
size="sm"
|
);
|
||||||
className="pb-0"
|
expirationBanner = (
|
||||||
onClick={onLearnMore}
|
<AccessExpirationDateBanner
|
||||||
aria-labelledby="outline-sidebar-upgrade-header"
|
accessExpirationDate={accessExpirationDate}
|
||||||
>
|
timezoneFormatArgs={timezoneFormatArgs}
|
||||||
{intl.formatMessage(messages.learnMore)}
|
/>
|
||||||
</Button>
|
);
|
||||||
</div>
|
upsellMessage = <UpsellFBEFarAwayCardContent />;
|
||||||
)}
|
} else { // more urgent messaging if there's less than 7 days left
|
||||||
</div>
|
upgradeCardHeaderText = (
|
||||||
</div>
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.accessExpirationUrgent"
|
||||||
|
defaultMessage="Course Access Expiration"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expirationBanner = <ExpirationCountdown hoursToExpiration={hoursToAccessExpiration} />;
|
||||||
|
upsellMessage = (
|
||||||
|
<UpsellFBESoonCardContent
|
||||||
|
accessExpirationDate={accessExpirationDate}
|
||||||
|
timezoneFormatArgs={timezoneFormatArgs}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // FBE is turned off
|
||||||
|
upgradeCardHeaderText = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.outline.alert.upgradecard.pursueAverifiedCertificate"
|
||||||
|
defaultMessage="Pursue a verified certificate"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
upsellMessage = (<UpsellNoFBECardContent />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mb-4 card">
|
||||||
|
<h2 className="h5 m-3" id="outline-sidebar-upgrade-header">
|
||||||
|
{upgradeCardHeaderText}
|
||||||
|
</h2>
|
||||||
|
{expirationBanner}
|
||||||
|
<div className="p-3">
|
||||||
|
{upsellMessage}
|
||||||
</div>
|
</div>
|
||||||
|
<UpgradeButton
|
||||||
|
offer={offer}
|
||||||
|
onClick={logClick}
|
||||||
|
verifiedMode={verifiedMode}
|
||||||
|
className="ml-3 mr-3 mb-3"
|
||||||
|
/>
|
||||||
|
{offerCode}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpgradeCard.propTypes = {
|
UpgradeCard.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
onLearnMore: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
UpgradeCard.defaultProps = {
|
|
||||||
onLearnMore: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(UpgradeCard);
|
export default injectIntl(UpgradeCard);
|
||||||
|
|||||||
@@ -2,3 +2,19 @@
|
|||||||
border: 1px solid $dark-500;
|
border: 1px solid $dark-500;
|
||||||
border-top: 5px solid $dark-500;
|
border-top: 5px solid $dark-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upsell-warning{
|
||||||
|
background-color: #FCF1F4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upsell-warning-light{
|
||||||
|
background-color: #FFFAED;;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-info {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.125);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-link-underline {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
@@ -118,6 +118,7 @@ function normalizeTabUrls(id, tabs) {
|
|||||||
|
|
||||||
function normalizeMetadata(metadata) {
|
function normalizeMetadata(metadata) {
|
||||||
return {
|
return {
|
||||||
|
timeOffsetMillis: metadata.timeOffsetMillis, // This should move to a global time correction reference
|
||||||
accessExpiration: camelCaseObject(metadata.access_expiration),
|
accessExpiration: camelCaseObject(metadata.access_expiration),
|
||||||
canShowUpgradeSock: metadata.can_show_upgrade_sock,
|
canShowUpgradeSock: metadata.can_show_upgrade_sock,
|
||||||
contentTypeGatingEnabled: metadata.content_type_gating_enabled,
|
contentTypeGatingEnabled: metadata.content_type_gating_enabled,
|
||||||
@@ -156,7 +157,23 @@ function normalizeMetadata(metadata) {
|
|||||||
export async function getCourseMetadata(courseId) {
|
export async function getCourseMetadata(courseId) {
|
||||||
let url = `${getConfig().LMS_BASE_URL}/api/courseware/course/${courseId}`;
|
let url = `${getConfig().LMS_BASE_URL}/api/courseware/course/${courseId}`;
|
||||||
url = appendBrowserTimezoneToUrl(url);
|
url = appendBrowserTimezoneToUrl(url);
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const requestTime = Date.now();
|
||||||
|
const { data, headers } = await getAuthenticatedHttpClient().get(url);
|
||||||
|
const responseTime = Date.now();
|
||||||
|
|
||||||
|
// Time offset computation should move down into the HttpClient wrapper to maintain a global time correction reference
|
||||||
|
// Requires 'Access-Control-Expose-Headers: Date' on the server response per https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-expose-headers
|
||||||
|
const headerDate = headers.date;
|
||||||
|
|
||||||
|
let timeOffsetMillis = 0;
|
||||||
|
if (headerDate !== undefined) {
|
||||||
|
const headerTime = Date.parse(headerDate);
|
||||||
|
const roundTripMillis = requestTime - responseTime;
|
||||||
|
const localTime = responseTime - (roundTripMillis / 2); // Roughly compensate for transit time
|
||||||
|
timeOffsetMillis = headerTime - localTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.timeOffsetMillis = timeOffsetMillis; // This should move to a global time correction reference
|
||||||
return normalizeMetadata(data);
|
return normalizeMetadata(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user