Skip to content

Commit

Permalink
feat: added UI for one click unsubscribe flow (#1444)
Browse files Browse the repository at this point in the history
* feat: added UI for one click unsubscribe flow

---------

Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
  • Loading branch information
muhammadadeeltajamul and awais-ansari authored Aug 19, 2024
1 parent 7cbbc72 commit a681333
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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';
2 changes: 2 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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>}
Expand Down
10 changes: 10 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
11 changes: 11 additions & 0 deletions src/preferences-unsubscribe/data/api.js
Original file line number Diff line number Diff line change
@@ -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 changes: 89 additions & 0 deletions src/preferences-unsubscribe/index.jsx
Original file line number Diff line number Diff line change
@@ -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 changes: 75 additions & 0 deletions src/preferences-unsubscribe/index.test.jsx
Original file line number Diff line number Diff line change
@@ -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 changes: 30 additions & 0 deletions src/preferences-unsubscribe/messages.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit a681333

Please sign in to comment.