Skip to content

Commit c695f61

Browse files
committed
feat: added confirm email banner for unverified users
1 parent ec800dd commit c695f61

File tree

6 files changed

+344
-1
lines changed

6 files changed

+344
-1
lines changed

src/assets/confirm-email.svg

Lines changed: 76 additions & 0 deletions
Loading
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { useState } from 'react';
2+
3+
import {
4+
Button,
5+
Image,
6+
MarketingModal,
7+
ModalDialog,
8+
PageBanner,
9+
} from '@openedx/paragon';
10+
import { useDispatch, useSelector } from 'react-redux';
11+
12+
import { useIntl } from '@edx/frontend-platform/i18n';
13+
14+
import confirmEmailSVG from '../../assets/confirm-email.svg';
15+
import { selectIsEmailVerified } from '../data/selectors';
16+
import { sendAccountActivationEmail } from '../posts/data/thunks';
17+
import messages from './messages';
18+
19+
const DiscussionsConfirmEmailBanner = () => {
20+
const intl = useIntl();
21+
const dispatch = useDispatch();
22+
const isEmailVerified = useSelector(selectIsEmailVerified);
23+
const [showPageBanner, setShowPageBanner] = useState(!isEmailVerified);
24+
const [showConfirmModal, setShowConfirmModal] = useState(false);
25+
const closePageBanner = () => setShowPageBanner(false);
26+
const closeConfirmModal = () => setShowConfirmModal(false);
27+
const openConfirmModal = () => setShowConfirmModal(true);
28+
29+
if (isEmailVerified) { return null; }
30+
31+
const openConfirmModalButtonClick = () => {
32+
dispatch(sendAccountActivationEmail());
33+
openConfirmModal();
34+
};
35+
36+
const userConfirmEmailButtonClick = () => {
37+
closeConfirmModal();
38+
closePageBanner();
39+
};
40+
41+
return (
42+
<>
43+
<PageBanner show={showPageBanner} dismissible onDismiss={closePageBanner}>
44+
{intl.formatMessage(messages.confirmEmailTextReminderBanner, {
45+
confirmNowButton: (
46+
<Button
47+
className="confirm-email-now-button"
48+
variant="link"
49+
size="inline"
50+
onClick={openConfirmModalButtonClick}
51+
>
52+
{intl.formatMessage(messages.confirmNowButton)}
53+
</Button>
54+
),
55+
})}
56+
</PageBanner>
57+
<MarketingModal
58+
title=""
59+
isOpen={showConfirmModal}
60+
onClose={closeConfirmModal}
61+
hasCloseButton={false}
62+
heroNode={(
63+
<ModalDialog.Hero className="bg-gray-300">
64+
<Image
65+
className="m-auto"
66+
src={confirmEmailSVG}
67+
alt={intl.formatMessage(messages.confirmEmailImageAlt)}
68+
/>
69+
</ModalDialog.Hero>
70+
)}
71+
footerNode={(
72+
<Button className="mx-auto my-3" variant="danger" onClick={userConfirmEmailButtonClick}>
73+
{intl.formatMessage(messages.verifiedConfirmEmailButton)}
74+
</Button>
75+
)}
76+
>
77+
<h2 className="text-center p-3 h1">{intl.formatMessage(messages.confirmEmailModalHeader)}</h2>
78+
<p className="text-center">{intl.formatMessage(messages.confirmEmailModalBody)}</p>
79+
</MarketingModal>
80+
</>
81+
);
82+
};
83+
84+
DiscussionsConfirmEmailBanner.propTypes = {};
85+
86+
export default DiscussionsConfirmEmailBanner;
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {
2+
fireEvent, render, screen, waitFor,
3+
} from '@testing-library/react';
4+
import MockAdapter from 'axios-mock-adapter';
5+
import { act } from 'react-dom/test-utils';
6+
import { IntlProvider } from 'react-intl';
7+
import { Context as ResponsiveContext } from 'react-responsive';
8+
9+
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
10+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
11+
import { AppProvider } from '@edx/frontend-platform/react';
12+
13+
import { initializeStore } from '../../store';
14+
import executeThunk from '../../test-utils';
15+
import { getDiscussionsConfigUrl } from '../data/api';
16+
import fetchCourseConfig from '../data/thunks';
17+
import DiscussionsConfirmEmailBanner from './DiscussionsConfirmEmailBanner';
18+
import messages from './messages';
19+
20+
const courseId = 'course-v1:edX+DemoX+Demo_Course';
21+
let axiosMock;
22+
let store;
23+
24+
function renderComponent() {
25+
render(
26+
<IntlProvider locale="en">
27+
<ResponsiveContext.Provider value={{ width: 1280 }}>
28+
<AppProvider store={store}>
29+
<DiscussionsConfirmEmailBanner />
30+
</AppProvider>
31+
</ResponsiveContext.Provider>
32+
</IntlProvider>,
33+
);
34+
}
35+
36+
describe('DiscussionsConfirmEmailBanner', () => {
37+
beforeEach(async () => {
38+
initializeMockApp({
39+
authenticatedUser: {
40+
userId: 3,
41+
username: 'abc123',
42+
administrator: true,
43+
roles: [],
44+
},
45+
});
46+
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
47+
store = initializeStore();
48+
});
49+
50+
describe('render', () => {
51+
it('does not show when email is verified', async () => {
52+
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { isEmailVerified: true });
53+
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
54+
renderComponent();
55+
const banner = screen.queryByRole('alert');
56+
expect(banner).toBeNull();
57+
});
58+
59+
describe('when email is unverified', () => {
60+
let resendEmailUrl;
61+
beforeEach(async () => {
62+
resendEmailUrl = `${getConfig().LMS_BASE_URL}/api/send_account_activation_email`;
63+
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { isEmailVerified: false });
64+
axiosMock.onPost(resendEmailUrl).reply(200);
65+
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
66+
renderComponent();
67+
});
68+
69+
it('shows banner', async () => {
70+
const banner = await screen.findByRole('alert');
71+
expect(banner.textContent).toContain('Remember to confirm');
72+
});
73+
74+
it('shows confirm now button', async () => {
75+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
76+
expect(confirmButton).toBeInTheDocument();
77+
});
78+
79+
it('shows modal when confirm now button is clicked', async () => {
80+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
81+
await act(async () => {
82+
fireEvent.click(confirmButton);
83+
});
84+
await waitFor(() => {
85+
const modal = screen.getByRole('dialog');
86+
expect(modal).toBeInTheDocument();
87+
});
88+
});
89+
90+
it('shows modal header and body', async () => {
91+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
92+
await act(async () => {
93+
fireEvent.click(confirmButton);
94+
});
95+
await waitFor(() => {
96+
const modalHeader = screen.getByText(messages.confirmEmailModalHeader.defaultMessage);
97+
expect(modalHeader).toBeInTheDocument();
98+
const modalBody = screen.getByText(messages.confirmEmailModalBody.defaultMessage);
99+
expect(modalBody).toBeInTheDocument();
100+
});
101+
});
102+
103+
it('shows confirm image', async () => {
104+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
105+
await act(async () => {
106+
fireEvent.click(confirmButton);
107+
});
108+
await waitFor(() => {
109+
const confirmImage = screen.getByRole('img', { name: messages.confirmEmailImageAlt.defaultMessage });
110+
expect(confirmImage).toBeInTheDocument();
111+
});
112+
});
113+
114+
it('shows confirm email button', async () => {
115+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
116+
await act(async () => {
117+
fireEvent.click(confirmButton);
118+
});
119+
await waitFor(() => {
120+
const verifyButton = screen.getByRole('button', { name: messages.verifiedConfirmEmailButton.defaultMessage });
121+
expect(verifyButton).toBeInTheDocument();
122+
});
123+
});
124+
125+
it('calls resend email API when confirm now button is clicked', async () => {
126+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
127+
await act(async () => {
128+
fireEvent.click(confirmButton);
129+
});
130+
await waitFor(() => expect(axiosMock.history.post.length).toBe(1));
131+
expect(axiosMock.history.post[0].url).toBe(resendEmailUrl);
132+
});
133+
});
134+
});
135+
});

src/discussions/discussions-home/DiscussionsHome.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const DiscussionsProductTour = lazy(() => import('../tours/DiscussionsProductTou
3636
const DiscussionsRestrictionBanner = lazy(() => import('./DiscussionsRestrictionBanner'));
3737
const DiscussionContent = lazy(() => import('./DiscussionContent'));
3838
const DiscussionSidebar = lazy(() => import('./DiscussionSidebar'));
39+
const DiscussionsConfirmEmailBanner = lazy(() => import('./DiscussionsConfirmEmailBanner'));
3940

4041
const DiscussionsHome = () => {
4142
const location = useLocation();
@@ -81,7 +82,12 @@ const DiscussionsHome = () => {
8182
return (
8283
<Suspense fallback={(<Spinner />)}>
8384
<DiscussionContext.Provider value={discussionContextValue}>
84-
{!enableInContextSidebar && (<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />)}
85+
{!enableInContextSidebar && (
86+
<>
87+
<DiscussionsConfirmEmailBanner />
88+
<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />
89+
</>
90+
)}
8591
<main className="container-fluid d-flex flex-column p-0 w-100 font-size" id="main" tabIndex="-1">
8692
{!enableInContextSidebar && <CourseTabsNavigation />}
8793
{(isEnrolled || !isUserLearner) && (
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { defineMessages } from '@edx/frontend-platform/i18n';
2+
3+
const messages = defineMessages({
4+
confirmNowButton: {
5+
id: 'discussions.confirmEmailBanner',
6+
description: 'Button for sending confirm email and open modal',
7+
defaultMessage: 'Confirm Now',
8+
},
9+
confirmEmailTextReminderBanner: {
10+
id: 'discussions.confirmEmailTextReminderBanner',
11+
description: 'Text for reminding user to confirm email',
12+
defaultMessage: 'Remember to confirm your email so that you can keep posting! {confirmNowButton}.',
13+
},
14+
verifiedConfirmEmailButton: {
15+
id: 'discussions.verifiedConfirmEmailButton',
16+
description: 'Button for verified confirming email',
17+
defaultMessage: 'I\'ve confirmed my email',
18+
},
19+
confirmEmailModalHeader: {
20+
id: 'discussions.confirmEmailModalHeader',
21+
description: 'title for confirming email modal',
22+
defaultMessage: 'Confirm your email',
23+
},
24+
confirmEmailModalBody: {
25+
id: 'discussions.confirmEmailModalBody',
26+
description: 'text hint for confirming email modal',
27+
defaultMessage: 'We\'ve sent you an email to verify your account. Please check your inbox and click on the big red button to confirm and keep learning.',
28+
},
29+
confirmEmailImageAlt: {
30+
id: 'discussions.confirmEmailImageAlt',
31+
description: 'text alt confirm email image',
32+
defaultMessage: 'confirm email background',
33+
},
34+
});
35+
36+
export default messages;

src/index.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,10 @@ th, td {
602602
width: 16px !important;
603603
}
604604

605+
.confirm-email-now-button {
606+
text-decoration: underline !important;
607+
}
608+
605609
@media only screen and (max-width: 367px) {
606610

607611
.discussion-comments h5,

0 commit comments

Comments
 (0)