Skip to content

Commit 76fabbf

Browse files
authored
feat: added confirm email banner for unverified users (#801)
* feat: added confirm email banner for unverified users * test: improved test coverage * refactor: refactored code
1 parent 5d13622 commit 76fabbf

File tree

6 files changed

+319
-1
lines changed

6 files changed

+319
-1
lines changed

src/assets/confirm-email.svg

Lines changed: 76 additions & 0 deletions
Loading
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React, { useCallback, 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 = useCallback(() => setShowPageBanner(false), [setShowPageBanner]);
26+
const closeConfirmModal = useCallback(() => setShowConfirmModal(false), [setShowConfirmModal]);
27+
const openConfirmModal = useCallback(() => setShowConfirmModal(true), [setShowConfirmModal]);
28+
29+
const handleConfirmNowClick = useCallback(() => {
30+
dispatch(sendAccountActivationEmail());
31+
openConfirmModal();
32+
closePageBanner();
33+
}, [dispatch, openConfirmModal, closePageBanner]);
34+
35+
const handleVerifiedClick = useCallback(() => {
36+
closeConfirmModal();
37+
closePageBanner();
38+
}, [closeConfirmModal, closePageBanner]);
39+
40+
if (isEmailVerified) { return null; }
41+
42+
return (
43+
<>
44+
<PageBanner show={showPageBanner} dismissible onDismiss={closePageBanner}>
45+
{intl.formatMessage(messages.confirmEmailTextReminderBanner, {
46+
confirmNowButton: (
47+
<Button
48+
className="confirm-email-now-button"
49+
variant="link"
50+
size="inline"
51+
onClick={handleConfirmNowClick}
52+
>
53+
{intl.formatMessage(messages.confirmNowButton)}
54+
</Button>
55+
),
56+
})}
57+
</PageBanner>
58+
<MarketingModal
59+
title=""
60+
isOpen={showConfirmModal}
61+
onClose={closeConfirmModal}
62+
hasCloseButton={false}
63+
heroNode={(
64+
<ModalDialog.Hero className="bg-gray-300">
65+
<Image
66+
className="m-auto"
67+
src={confirmEmailSVG}
68+
alt={intl.formatMessage(messages.confirmEmailImageAlt)}
69+
/>
70+
</ModalDialog.Hero>
71+
)}
72+
footerNode={(
73+
<Button className="mx-auto my-3" variant="danger" onClick={handleVerifiedClick}>
74+
{intl.formatMessage(messages.verifiedConfirmEmailButton)}
75+
</Button>
76+
)}
77+
>
78+
<h2 className="text-center p-3 h1">{intl.formatMessage(messages.confirmEmailModalHeader)}</h2>
79+
<p className="text-center">{intl.formatMessage(messages.confirmEmailModalBody)}</p>
80+
</MarketingModal>
81+
</>
82+
);
83+
};
84+
85+
export default DiscussionsConfirmEmailBanner;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
expect(screen.queryByRole('alert')).toBeNull();
56+
});
57+
58+
describe('when email is unverified', () => {
59+
let resendEmailUrl;
60+
beforeEach(async () => {
61+
resendEmailUrl = `${getConfig().LMS_BASE_URL}/api/send_account_activation_email`;
62+
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { isEmailVerified: false });
63+
axiosMock.onPost(resendEmailUrl).reply(200);
64+
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
65+
renderComponent();
66+
});
67+
68+
it('shows banner and confirm now button', async () => {
69+
const banner = await screen.findByRole('alert');
70+
expect(banner.textContent).toContain('Remember to confirm');
71+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
72+
expect(confirmButton).toBeInTheDocument();
73+
});
74+
75+
it('opens modal, closes banner, and calls resend email API when confirm now button is clicked', async () => {
76+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
77+
await act(async () => {
78+
fireEvent.click(confirmButton);
79+
});
80+
await waitFor(() => {
81+
expect(screen.getByRole('dialog')).toBeInTheDocument();
82+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
83+
expect(axiosMock.history.post).toHaveLength(1);
84+
expect(axiosMock.history.post[0].url).toBe(resendEmailUrl);
85+
});
86+
});
87+
88+
it('shows modal header, body, image, and confirm email button and closes modal and banner on click', async () => {
89+
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
90+
await act(async () => {
91+
fireEvent.click(confirmButton);
92+
});
93+
await waitFor(() => {
94+
expect(screen.getByText(messages.confirmEmailModalHeader.defaultMessage)).toBeInTheDocument();
95+
expect(screen.getByText(messages.confirmEmailModalBody.defaultMessage)).toBeInTheDocument();
96+
expect(screen.getByRole('img', { name: messages.confirmEmailImageAlt.defaultMessage })).toBeInTheDocument();
97+
98+
const verifyButton = screen.getByRole('button', { name: messages.verifiedConfirmEmailButton.defaultMessage });
99+
expect(verifyButton).toBeInTheDocument();
100+
act(() => {
101+
fireEvent.click(verifyButton);
102+
});
103+
});
104+
await waitFor(() => {
105+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
106+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
107+
});
108+
});
109+
});
110+
});
111+
});

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)