feat: added UI for one click unsubscribe flow (#1444)
* feat: added UI for one click unsubscribe flow --------- Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
This commit is contained in:
committed by
GitHub
parent
7cbbc720d1
commit
a681333a08
@@ -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';
|
||||
|
||||
@@ -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, () => {
|
||||
<Routes>
|
||||
<Route path={ROUTES.UNSUBSCRIBE} element={<PageWrap><GoalUnsubscribe /></PageWrap>} />
|
||||
<Route path={ROUTES.REDIRECT} element={<PageWrap><CoursewareRedirectLandingPage /></PageWrap>} />
|
||||
<Route path={ROUTES.PREFERENCES_UNSUBSCRIBE} element={<PageWrap><PreferencesUnsubscribe /></PageWrap>} />
|
||||
<Route
|
||||
path={DECODE_ROUTES.ACCESS_DENIED}
|
||||
element={<DecodePageRoute><CourseAccessErrorPage /></DecodePageRoute>}
|
||||
|
||||
@@ -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";
|
||||
|
||||
11
src/preferences-unsubscribe/data/api.js
Normal file
11
src/preferences-unsubscribe/data/api.js
Normal file
@@ -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);
|
||||
}
|
||||
89
src/preferences-unsubscribe/index.jsx
Normal file
89
src/preferences-unsubscribe/index.jsx
Normal file
@@ -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 (
|
||||
<div style={{ height: '100vh' }}>
|
||||
<Header />
|
||||
<Container size="xs" className="h-75 mx-auto my-auto">
|
||||
<div className="d-flex flex-row h-100">
|
||||
<div className="mx-auto my-auto">
|
||||
{status === LOADING && <PageLoading srMessage={`${intl.formatMessage(messages.unsubscribeLoading)}`} />}
|
||||
{status !== LOADING && (
|
||||
<>
|
||||
<Icon src={pageContent.icon} className={`size-56px mx-auto ${pageContent.iconClass}`} />
|
||||
<h3 className="font-weight-bold text-primary-500 text-center my-3" data-testid="heading-text">
|
||||
{intl.formatMessage(pageContent.headingText)}
|
||||
</h3>
|
||||
<div className="font-weight-normal text-gray-700 text-center">
|
||||
{intl.formatMessage(pageContent.bodyText)}
|
||||
</div>
|
||||
<small className="d-block font-weight-normal text-gray text-center mt-3">
|
||||
<FormattedMessage
|
||||
id="learning.notification.preferences.unsubscribe.preferenceCenterUrl"
|
||||
description="Shown as a suggestion or recommendation for learner when their unsubscribing request has failed"
|
||||
defaultMessage="Go to the {preferenceCenterUrl} to set your preferences"
|
||||
values={{
|
||||
preferenceCenterUrl: (
|
||||
<Hyperlink
|
||||
destination={`${getConfig().ACCOUNT_SETTINGS_URL}/notifications`}
|
||||
>
|
||||
{intl.formatMessage(messages.preferenceCenterUrl)}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreferencesUnsubscribe;
|
||||
75
src/preferences-unsubscribe/index.test.jsx
Normal file
75
src/preferences-unsubscribe/index.test.jsx
Normal file
@@ -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 = (
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<UserMessagesProvider>
|
||||
<MemoryRouter initialEntries={[`${`/preferences-unsubscribe/${userToken}/${updatePatch}/`}`]}>
|
||||
<Routes>
|
||||
<Route path={ROUTES.PREFERENCES_UNSUBSCRIBE} element={<PreferencesUnsubscribe />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</UserMessagesProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
30
src/preferences-unsubscribe/messages.js
Normal file
30
src/preferences-unsubscribe/messages.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user