diff --git a/package-lock.json b/package-lock.json
index 20b78564a0..deba109501 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13886,6 +13886,11 @@
}
}
},
+ "js-cookie": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
+ "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index 7099a250ca..8d75655f4d 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"@reduxjs/toolkit": "1.3.6",
"classnames": "2.2.6",
"core-js": "3.6.5",
+ "js-cookie": "2.2.1",
"lodash.camelcase": "^4.3.0",
"prop-types": "15.7.2",
"react": "16.13.1",
diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx
index 2220d63e48..6c2b5f21b0 100644
--- a/src/courseware/course/Course.jsx
+++ b/src/courseware/course/Course.jsx
@@ -1,7 +1,8 @@
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useDispatch } from 'react-redux';
+import Cookies from 'js-cookie';
import { getConfig } from '@edx/frontend-platform';
import { AlertList } from '../../generic/user-messages';
@@ -13,8 +14,11 @@ import Sequence from './sequence';
import { CelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
+import SidebarNotificationButton from './SidebarNotificationButton';
+
import CourseSock from '../../generic/course-sock';
import { useModel } from '../../generic/model-store';
+import useWindowSize, { responsiveBreakpoints } from '../../generic/tabs/useWindowSize';
/** [MM-P2P] Experiment */
import { initCoursewareMMP2P, MMP2PBlockModal } from '../../experiments/mm-p2p';
@@ -57,6 +61,19 @@ function Course({
courseId, sequenceId, unitId, celebrateFirstSection, dispatch, celebrations,
);
+ // REV-2130 TODO: temporary cookie code that should be removed.
+ // In order to see the Value Prop sidebar in prod, a cookie should be set in
+ // the browser console and refresh: document.cookie = 'value_prop_cookie=true';
+ const isValuePropCookieSet = Cookies.get('value_prop_cookie') === 'true';
+
+ const shouldDisplaySidebarButton = useWindowSize().width >= responsiveBreakpoints.small.minWidth;
+
+ const [sidebarVisible, setSidebar] = useState(false);
+ const isSidebarVisible = () => sidebarVisible && setSidebar;
+ const toggleSidebar = () => {
+ if (sidebarVisible) { setSidebar(false); } else { setSidebar(true); }
+ };
+
/** [MM-P2P] Experiment */
const MMP2P = initCoursewareMMP2P(courseId, sequenceId, unitId);
@@ -76,13 +93,23 @@ function Course({
}}
/>
)}
-
+
+
+
+ { isValuePropCookieSet && shouldDisplaySidebarButton ? (
+
+ ) : null}
+
+
diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx
index 389ce8ddd5..4edd9a1c90 100644
--- a/src/courseware/course/Course.test.jsx
+++ b/src/courseware/course/Course.test.jsx
@@ -1,5 +1,6 @@
import React from 'react';
import { Factory } from 'rosie';
+import Cookies from 'js-cookie';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
loadUnit, render, screen, waitFor, getByRole, initializeTestStore, fireEvent,
@@ -19,6 +20,7 @@ describe('Course', () => {
nextSequenceHandler: () => {},
previousSequenceHandler: () => {},
unitNavigationHandler: () => {},
+ toggleSidebar: () => {},
};
beforeAll(async () => {
@@ -85,6 +87,31 @@ describe('Course', () => {
expect(screen.getByRole('button', { name: 'Learn About Verified Certificates' })).toBeInTheDocument();
});
+ it('displays sidebar notification button', async () => {
+ const toggleSidebar = jest.fn();
+ const isSidebarVisible = jest.fn();
+
+ // REV-2130 TODO: remove cookie related code once temporary value prop cookie code is removed.
+ const cookieName = 'value_prop_cookie';
+ Cookies.set = jest.fn();
+ Cookies.get = jest.fn().mockImplementation(() => cookieName);
+ const getSpy = jest.spyOn(Cookies, 'get').mockReturnValueOnce('true');
+
+ const courseMetadata = Factory.build('courseMetadata');
+ const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
+ const testData = {
+ ...mockData,
+ toggleSidebar,
+ isSidebarVisible,
+ };
+ render(, { store: testStore });
+
+ const sidebarOpenButton = screen.getByRole('button', { name: /Show sidebar notification/i });
+
+ expect(getSpy).toBeCalledWith(cookieName);
+ expect(sidebarOpenButton).toBeInTheDocument();
+ });
+
it('displays offer and expiration alert', async () => {
const courseMetadata = Factory.build('courseMetadata', {
access_expiration: {
diff --git a/src/courseware/course/CourseBreadcrumbs.jsx b/src/courseware/course/CourseBreadcrumbs.jsx
index a90dd5e4b3..d8a73c8d70 100644
--- a/src/courseware/course/CourseBreadcrumbs.jsx
+++ b/src/courseware/course/CourseBreadcrumbs.jsx
@@ -60,7 +60,7 @@ export default function CourseBreadcrumbs({
}, [courseStatus, sequenceStatus]);
return (
-