diff --git a/.env b/.env
index c4e0195..889b922 100644
--- a/.env
+++ b/.env
@@ -37,3 +37,4 @@ HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
+ENABLE_NOTICES=''
diff --git a/.env.development b/.env.development
index ed25984..4abfc76 100644
--- a/.env.development
+++ b/.env.development
@@ -44,3 +44,4 @@ HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
+ENABLE_NOTICES=''
diff --git a/.env.test b/.env.test
index 4116383..4b53e6b 100644
--- a/.env.test
+++ b/.env.test
@@ -43,3 +43,4 @@ HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
+ENABLE_NOTICES=''
diff --git a/src/__snapshots__/index.test.jsx.snap b/src/__snapshots__/index.test.jsx.snap
index 69bd824..8ffdbfb 100644
--- a/src/__snapshots__/index.test.jsx.snap
+++ b/src/__snapshots__/index.test.jsx.snap
@@ -14,15 +14,17 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
}
}
>
-
-
-
-
-
-
+
+
+
+
+
+
+
+
`;
diff --git a/src/components/NoticesWrapper/api.js b/src/components/NoticesWrapper/api.js
new file mode 100644
index 0000000..1b505c3
--- /dev/null
+++ b/src/components/NoticesWrapper/api.js
@@ -0,0 +1,26 @@
+import { getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
+import { logError, logInfo } from '@edx/frontend-platform/logging';
+
+export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
+export const error404Message = 'This probably happened because the notices plugin is not installed on platform.';
+
+export const getNotices = ({ onLoad }) => {
+ const authenticatedUser = getAuthenticatedUser();
+
+ const handleError = async (e) => {
+ // Error probably means that notices is not installed, which is fine.
+ const { customAttributes: { httpErrorStatus } } = e;
+ if (httpErrorStatus === 404) {
+ logInfo(`${e}. ${error404Message}`);
+ } else {
+ logError(e);
+ }
+ };
+ if (authenticatedUser) {
+ return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
+ }
+ return null;
+};
+
+export default { getNotices };
diff --git a/src/components/NoticesWrapper/api.test.js b/src/components/NoticesWrapper/api.test.js
new file mode 100644
index 0000000..4470f6e
--- /dev/null
+++ b/src/components/NoticesWrapper/api.test.js
@@ -0,0 +1,65 @@
+import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
+import { logError, logInfo } from '@edx/frontend-platform/logging';
+
+import * as api from './api';
+
+jest.mock('@edx/frontend-platform', () => ({
+ getConfig: jest.fn(() => ({
+ LMS_BASE_URL: 'test-lms-url',
+ })),
+}));
+
+jest.mock('@edx/frontend-platform/auth', () => ({
+ getAuthenticatedHttpClient: jest.fn(),
+ getAuthenticatedUser: jest.fn(),
+}));
+
+jest.mock('@edx/frontend-platform/logging', () => ({
+ logError: jest.fn(),
+ logInfo: jest.fn(),
+}));
+
+const testData = 'test-data';
+const successfulGet = () => Promise.resolve(testData);
+const error404 = { customAttributes: { httpErrorStatus: 404 }, test: 'error' };
+const error404Get = () => Promise.reject(error404);
+const error500 = { customAttributes: { httpErrorStatus: 500 }, test: 'error' };
+const error500Get = () => Promise.reject(error500);
+
+const get = jest.fn().mockImplementation(successfulGet);
+getAuthenticatedHttpClient.mockReturnValue({ get });
+const authenticatedUser = { fake: 'user' };
+getAuthenticatedUser.mockReturnValue(authenticatedUser);
+
+const onLoad = jest.fn();
+describe('getNotices api method', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ describe('behavior', () => {
+ describe('not authenticated', () => {
+ it('does not fetch anything', () => {
+ getAuthenticatedUser.mockReturnValueOnce(null);
+ api.getNotices({ onLoad });
+ expect(get).not.toHaveBeenCalled();
+ });
+ });
+ describe('authenticated', () => {
+ it('fetches noticesUrl with onLoad behavior', async () => {
+ await api.getNotices({ onLoad });
+ expect(get).toHaveBeenCalledWith(api.noticesUrl, {});
+ expect(onLoad).toHaveBeenCalledWith(testData);
+ });
+ it('calls logInfo if fetch fails with 404', async () => {
+ get.mockImplementation(error404Get);
+ await api.getNotices({ onLoad });
+ expect(logInfo).toHaveBeenCalledWith(`${error404}. ${api.error404Message}`);
+ });
+ it('calls logError if fetch fails with non-404 error', async () => {
+ get.mockImplementation(error500Get);
+ await api.getNotices({ onLoad });
+ expect(logError).toHaveBeenCalledWith(error500);
+ });
+ });
+ });
+});
diff --git a/src/components/NoticesWrapper/hooks.js b/src/components/NoticesWrapper/hooks.js
new file mode 100644
index 0000000..275c2ab
--- /dev/null
+++ b/src/components/NoticesWrapper/hooks.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { getConfig } from '@edx/frontend-platform';
+
+import { StrictDict } from 'utils';
+import { getNotices } from './api';
+import * as module from './hooks';
+
+/**
+ * This component uses the platform-plugin-notices plugin to function.
+ * If the user has an unacknowledged notice, they will be rerouted off
+ * course home and onto a full-screen notice page. If the plugin is not
+ * installed, or there are no notices, we just passthrough this component.
+ */
+export const state = StrictDict({
+ isRedirected: (val) => React.useState(val), // eslint-disable-line
+});
+
+export const useNoticesWrapperData = () => {
+ const [isRedirected, setIsRedirected] = module.state.isRedirected();
+ React.useEffect(() => {
+ if (getConfig().ENABLE_NOTICES) {
+ getNotices({
+ onLoad: (data) => {
+ if (data?.results?.length > 0) {
+ setIsRedirected(true);
+ window.location.replace(`${data.results[0]}?next=${window.location.href}`);
+ }
+ },
+ });
+ }
+ }, [setIsRedirected]);
+ return { isRedirected };
+};
+
+export default useNoticesWrapperData;
diff --git a/src/components/NoticesWrapper/hooks.test.js b/src/components/NoticesWrapper/hooks.test.js
new file mode 100644
index 0000000..78f5434
--- /dev/null
+++ b/src/components/NoticesWrapper/hooks.test.js
@@ -0,0 +1,83 @@
+import React from 'react';
+
+import { MockUseState } from 'testUtils';
+
+import { getConfig } from '@edx/frontend-platform';
+import { getNotices } from './api';
+import * as hooks from './hooks';
+
+jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
+jest.mock('./api', () => ({ getNotices: jest.fn() }));
+
+getConfig.mockReturnValue({ ENABLE_NOTICES: true });
+const state = new MockUseState(hooks);
+
+let hook;
+describe('NoticesWrapper hooks', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ describe('state hooks', () => {
+ state.testGetter(state.keys.isRedirected);
+ });
+ describe('useNoticesWrapperData', () => {
+ beforeEach(() => {
+ state.mock();
+ });
+ describe('behavior', () => {
+ it('initializes state hooks', () => {
+ hooks.useNoticesWrapperData();
+ expect(hooks.state.isRedirected).toHaveBeenCalledWith();
+ });
+ describe('effects', () => {
+ it('does not call notices if not enabled', () => {
+ getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
+ hooks.useNoticesWrapperData();
+ const [cb, prereqs] = React.useEffect.mock.calls[0];
+ expect(prereqs).toEqual([state.setState.isRedirected]);
+ cb();
+ expect(getNotices).not.toHaveBeenCalled();
+ });
+ describe('getNotices call (if enabled) onLoad behavior', () => {
+ it('does not redirect if there are no results', () => {
+ hooks.useNoticesWrapperData();
+ expect(React.useEffect).toHaveBeenCalled();
+ const [cb, prereqs] = React.useEffect.mock.calls[0];
+ expect(prereqs).toEqual([state.setState.isRedirected]);
+ cb();
+ expect(getNotices).toHaveBeenCalled();
+ const { onLoad } = getNotices.mock.calls[0][0];
+ onLoad({});
+ expect(state.setState.isRedirected).not.toHaveBeenCalled();
+ onLoad({ data: {} });
+ expect(state.setState.isRedirected).not.toHaveBeenCalled();
+ onLoad({ data: { results: [] } });
+ expect(state.setState.isRedirected).not.toHaveBeenCalled();
+ });
+ it('redirects and set isRedirected if results are returned', () => {
+ delete window.location;
+ window.location = { replace: jest.fn(), href: 'test-old-href' };
+ hooks.useNoticesWrapperData();
+ const [cb, prereqs] = React.useEffect.mock.calls[0];
+ expect(prereqs).toEqual([state.setState.isRedirected]);
+ cb();
+ expect(getNotices).toHaveBeenCalled();
+ const { onLoad } = getNotices.mock.calls[0][0];
+ const target = 'url-target';
+ onLoad({ results: [target] });
+ expect(state.setState.isRedirected).toHaveBeenCalledWith(true);
+ expect(window.location.replace).toHaveBeenCalledWith(
+ `${target}?next=${window.location.href}`,
+ );
+ });
+ });
+ });
+ });
+ describe('output', () => {
+ it('forwards isRedirected from state call', () => {
+ hook = hooks.useNoticesWrapperData();
+ expect(hook.isRedirected).toEqual(state.stateVals.isRedirected);
+ });
+ });
+ });
+});
diff --git a/src/components/NoticesWrapper/index.jsx b/src/components/NoticesWrapper/index.jsx
new file mode 100644
index 0000000..faf9b9f
--- /dev/null
+++ b/src/components/NoticesWrapper/index.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import useNoticesWrapperData from './hooks';
+
+/**
+ * This component uses the platform-plugin-notices plugin to function.
+ * If the user has an unacknowledged notice, they will be rerouted off
+ * course home and onto a full-screen notice page. If the plugin is not
+ * installed, or there are no notices, we just passthrough this component.
+ */
+const NoticesWrapper = ({ children }) => {
+ const { isRedirected } = useNoticesWrapperData();
+ return (
+
+ {isRedirected === true ? null : children}
+
+ );
+};
+
+NoticesWrapper.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+export default NoticesWrapper;
diff --git a/src/components/NoticesWrapper/index.test.jsx b/src/components/NoticesWrapper/index.test.jsx
new file mode 100644
index 0000000..f7f8b81
--- /dev/null
+++ b/src/components/NoticesWrapper/index.test.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import useNoticesWrapperData from './hooks';
+import NoticesWrapper from '.';
+
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = { isRedirected: false };
+useNoticesWrapperData.mockReturnValue(hookProps);
+
+let el;
+const children = [some, children];
+describe('NoticesWrapper component', () => {
+ describe('behavior', () => {
+ it('initializes hooks', () => {
+ el = shallow({children});
+ expect(useNoticesWrapperData).toHaveBeenCalledWith();
+ });
+ });
+ describe('output', () => {
+ it('does not show children if redirected', () => {
+ useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
+ el = shallow({children});
+ expect(el.children().length).toEqual(0);
+ });
+ it('shows children if not redirected', () => {
+ el = shallow({children});
+ expect(el.children().length).toEqual(2);
+ expect(el.children().at(0).matchesElement(children[0])).toEqual(true);
+ expect(el.children().at(1).matchesElement(children[1])).toEqual(true);
+ });
+ });
+});
diff --git a/src/config/index.js b/src/config/index.js
index 47cf334..5b39721 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -13,6 +13,7 @@ const configuration = {
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
ZENDESK_KEY: process.env.ZENDESK_KEY,
SUPPORT_URL: process.env.SUPPORT_URL || null,
+ ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
};
const features = {};
diff --git a/src/index.jsx b/src/index.jsx
index 02b5f87..6078eca 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -26,16 +26,19 @@ import { configuration } from './config';
import messages from './i18n';
import App from './App';
+import NoticesWrapper from './components/NoticesWrapper';
subscribe(APP_READY, () => {
ReactDOM.render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
,
document.getElementById('root'),
);
diff --git a/src/index.test.jsx b/src/index.test.jsx
index 364e278..11e999a 100644
--- a/src/index.test.jsx
+++ b/src/index.test.jsx
@@ -34,6 +34,7 @@ jest.mock('@edx/frontend-component-footer', () => ({
}));
jest.mock('data/store', () => ({ redux: 'store' }));
jest.mock('./App', () => 'App');
+jest.mock('components/NoticesWrapper', () => 'NoticesWrapper');
describe('app registry', () => {
let getElement;
diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx
index 5de1011..2b51b81 100644
--- a/src/test/app.test.jsx
+++ b/src/test/app.test.jsx
@@ -44,6 +44,7 @@ jest.unmock('hooks');
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'loaded-widget-sidebar');
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'no-courses-widget-sidebar');
+jest.mock('components/NoticesWrapper', () => 'notices-wrapper');
jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),