diff --git a/src/constants.js b/src/constants.js index 172f870691..7cd914a08a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,6 +20,7 @@ export const DECODE_ROUTES = { export const ROUTES = { UNSUBSCRIBE: '/goal-unsubscribe/:token', + PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch', REDIRECT: '/redirect/*', DASHBOARD: 'dashboard', ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard', @@ -49,3 +50,8 @@ export const WIDGETS = { DISCUSSIONS: 'DISCUSSIONS', NOTIFICATIONS: 'NOTIFICATIONS', }; + +export const LOADING = 'loading'; +export const LOADED = 'loaded'; +export const FAILED = 'failed'; +export const DENIED = 'denied'; diff --git a/src/index.jsx b/src/index.jsx index f390cb3106..c67fe3bd41 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -37,6 +37,7 @@ import LiveTab from './course-home/live-tab/LiveTab'; import CourseAccessErrorPage from './generic/CourseAccessErrorPage'; import DecodePageRoute from './decode-page-route'; import { DECODE_ROUTES, ROUTES } from './constants'; +import PreferencesUnsubscribe from './preferences-unsubscribe'; subscribe(APP_READY, () => { ReactDOM.render( @@ -50,6 +51,7 @@ subscribe(APP_READY, () => { } /> } /> + } /> } diff --git a/src/index.scss b/src/index.scss index 5b66af78a4..4f3477e613 100755 --- a/src/index.scss +++ b/src/index.scss @@ -434,6 +434,16 @@ min-height: 700px; } +.size-4r { + width: 4rem !important; + height: 4rem !important; +} + +.size-56px { + width: 56px !important; + height: 56px !important; +} + // Import component-specific sass files @import "courseware/course/celebration/CelebrationModal.scss"; @import "courseware/course/sidebar/sidebars/discussions/Discussions.scss"; diff --git a/src/preferences-unsubscribe/data/api.js b/src/preferences-unsubscribe/data/api.js new file mode 100644 index 0000000000..395f3c3144 --- /dev/null +++ b/src/preferences-unsubscribe/data/api.js @@ -0,0 +1,11 @@ +import { getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +export const getUnsubscribeUrl = (userToken, updatePatch) => ( + `${getConfig().LMS_BASE_URL}/api/notifications/preferences/update/${userToken}/${updatePatch}/` +); + +export async function unsubscribeNotificationPreferences(userToken, updatePatch) { + const url = getUnsubscribeUrl(userToken, updatePatch); + return getAuthenticatedHttpClient().get(url); +} diff --git a/src/preferences-unsubscribe/index.jsx b/src/preferences-unsubscribe/index.jsx new file mode 100644 index 0000000000..af9ba379c5 --- /dev/null +++ b/src/preferences-unsubscribe/index.jsx @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from 'react'; + +import { Container, Icon, Hyperlink } from '@openedx/paragon'; +import { CheckCircleLightOutline, ErrorOutline } from '@openedx/paragon/icons'; +import { useParams } from 'react-router-dom'; + +import Header from '@edx/frontend-component-header'; +import { getConfig } from '@edx/frontend-platform'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { logError } from '@edx/frontend-platform/logging'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; + +import { LOADED, LOADING, FAILED } from '../constants'; +import PageLoading from '../generic/PageLoading'; +import { unsubscribeNotificationPreferences } from './data/api'; +import messages from './messages'; + +const PreferencesUnsubscribe = () => { + const intl = useIntl(); + const { userToken, updatePatch } = useParams(); + const [status, setStatus] = useState(LOADING); + + useEffect(() => { + unsubscribeNotificationPreferences(userToken, updatePatch).then( + () => setStatus(LOADED), + (error) => { + setStatus(FAILED); + logError(error); + }, + ); + sendTrackEvent('edx.ui.lms.notifications.preferences.unsubscribe', { userToken, updatePatch }); + }, []); + + const pageContent = { + icon: CheckCircleLightOutline, + iconClass: 'text-success', + headingText: messages.unsubscribeSuccessHeading, + bodyText: messages.unsubscribeSuccessMessage, + }; + + if (status === FAILED) { + pageContent.icon = ErrorOutline; + pageContent.iconClass = 'text-danger'; + pageContent.headingText = messages.unsubscribeFailedHeading; + pageContent.bodyText = messages.unsubscribeFailedMessage; + } + + return ( +
+
+ +
+
+ {status === LOADING && } + {status !== LOADING && ( + <> + +

+ {intl.formatMessage(pageContent.headingText)} +

+
+ {intl.formatMessage(pageContent.bodyText)} +
+ + + {intl.formatMessage(messages.preferenceCenterUrl)} + + ), + }} + /> + + + )} +
+
+
+
+ ); +}; + +export default PreferencesUnsubscribe; diff --git a/src/preferences-unsubscribe/index.test.jsx b/src/preferences-unsubscribe/index.test.jsx new file mode 100644 index 0000000000..52cef01bb9 --- /dev/null +++ b/src/preferences-unsubscribe/index.test.jsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import MockAdapter from 'axios-mock-adapter'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; + +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { AppProvider } from '@edx/frontend-platform/react'; + +import { ROUTES } from '../constants'; +import { + initializeTestStore, initializeMockApp, render, screen, waitFor, +} from '../setupTest'; +import { getUnsubscribeUrl } from './data/api'; +import PreferencesUnsubscribe from './index'; +import initializeStore from '../store'; +import { UserMessagesProvider } from '../generic/user-messages'; + +initializeMockApp(); +jest.mock('@edx/frontend-platform/analytics'); + +describe('Notification Preferences One Click Unsubscribe', () => { + let axiosMock; + let component; + let store; + const userToken = '1234'; + const updatePatch = 'abc123'; + const url = getUnsubscribeUrl(userToken, updatePatch); + + beforeAll(async () => { + await initializeTestStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + }); + + beforeEach(() => { + sendTrackEvent.mockClear(); + axiosMock.reset(); + store = initializeStore(); + component = ( + + + + + } /> + + + + + ); + }); + + it('tests UI when unsubscribe is successful', async () => { + axiosMock.onGet(url).reply(200, { result: 'success' }); + render(component); + + await waitFor(() => expect(axiosMock.history.get).toHaveLength(1)); + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + + expect(screen.getByTestId('heading-text')).toHaveTextContent('Unsubscribe successful'); + }); + + it('tests UI when unsubscribe failed', async () => { + axiosMock.onGet(url).reply(400, { result: 'failed' }); + render(component); + + await waitFor(() => expect(axiosMock.history.get).toHaveLength(1)); + + expect(sendTrackEvent).toHaveBeenCalledTimes(1); + expect(screen.getByTestId('heading-text')).toHaveTextContent('Error unsubscribing from preference'); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.notifications.preferences.unsubscribe', { + userToken, + updatePatch, + }); + }); +}); diff --git a/src/preferences-unsubscribe/messages.js b/src/preferences-unsubscribe/messages.js new file mode 100644 index 0000000000..14364a6c00 --- /dev/null +++ b/src/preferences-unsubscribe/messages.js @@ -0,0 +1,30 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + unsubscribeLoading: { + id: 'learning.notification.preferences.unsubscribe.loading', + defaultMessage: 'Loading', + }, + unsubscribeSuccessHeading: { + id: 'learning.notification.preferences.unsubscribe.successHeading', + defaultMessage: 'Unsubscribe successful', + }, + unsubscribeSuccessMessage: { + id: 'learning.notification.preferences.unsubscribe.successMessage', + defaultMessage: 'You have successfully unsubscribed from email digests for learning activity', + }, + unsubscribeFailedHeading: { + id: 'learning.notification.preferences.unsubscribe.failedHeading', + defaultMessage: 'Error unsubscribing from preference', + }, + unsubscribeFailedMessage: { + id: 'learning.notification.preferences.unsubscribe.failedMessage', + defaultMessage: 'Invalid Url or token expired', + }, + preferenceCenterUrl: { + id: 'learning.notification.preferences.unsubscribe.preferenceCenterUrl', + defaultMessage: 'preferences page', + }, +}); + +export default messages;