Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/assets/confirm-email.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 85 additions & 0 deletions src/discussions/discussions-home/DiscussionsConfirmEmailBanner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useCallback, useState } from 'react';

import {
Button,
Image,
MarketingModal,
ModalDialog,
PageBanner,
} from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux';

import { useIntl } from '@edx/frontend-platform/i18n';

import confirmEmailSVG from '../../assets/confirm-email.svg';
import { selectIsEmailVerified } from '../data/selectors';
import { sendAccountActivationEmail } from '../posts/data/thunks';
import messages from './messages';

const DiscussionsConfirmEmailBanner = () => {
const intl = useIntl();
const dispatch = useDispatch();
const isEmailVerified = useSelector(selectIsEmailVerified);
const [showPageBanner, setShowPageBanner] = useState(!isEmailVerified);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const closePageBanner = useCallback(() => setShowPageBanner(false), [setShowPageBanner]);
const closeConfirmModal = useCallback(() => setShowConfirmModal(false), [setShowConfirmModal]);
const openConfirmModal = useCallback(() => setShowConfirmModal(true), [setShowConfirmModal]);

const handleConfirmNowClick = useCallback(() => {
dispatch(sendAccountActivationEmail());
openConfirmModal();
closePageBanner();
}, [dispatch, openConfirmModal, closePageBanner]);

const handleVerifiedClick = useCallback(() => {
closeConfirmModal();
closePageBanner();
}, [closeConfirmModal, closePageBanner]);

if (isEmailVerified) { return null; }

return (
<>
<PageBanner show={showPageBanner} dismissible onDismiss={closePageBanner}>
{intl.formatMessage(messages.confirmEmailTextReminderBanner, {
confirmNowButton: (
<Button
className="confirm-email-now-button"
variant="link"
size="inline"
onClick={handleConfirmNowClick}
>
{intl.formatMessage(messages.confirmNowButton)}
</Button>
),
})}
</PageBanner>
<MarketingModal
title=""
isOpen={showConfirmModal}
onClose={closeConfirmModal}
hasCloseButton={false}
heroNode={(
<ModalDialog.Hero className="bg-gray-300">
<Image
className="m-auto"
src={confirmEmailSVG}
alt={intl.formatMessage(messages.confirmEmailImageAlt)}
/>
</ModalDialog.Hero>
)}
footerNode={(
<Button className="mx-auto my-3" variant="danger" onClick={handleVerifiedClick}>
{intl.formatMessage(messages.verifiedConfirmEmailButton)}
</Button>
)}
>
<h2 className="text-center p-3 h1">{intl.formatMessage(messages.confirmEmailModalHeader)}</h2>
<p className="text-center">{intl.formatMessage(messages.confirmEmailModalBody)}</p>
</MarketingModal>
</>
);
};

export default DiscussionsConfirmEmailBanner;
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
fireEvent, render, screen, waitFor,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { act } from 'react-dom/test-utils';
import { IntlProvider } from 'react-intl';
import { Context as ResponsiveContext } from 'react-responsive';

import { getConfig, initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';

import { initializeStore } from '../../store';
import executeThunk from '../../test-utils';
import { getDiscussionsConfigUrl } from '../data/api';
import fetchCourseConfig from '../data/thunks';
import DiscussionsConfirmEmailBanner from './DiscussionsConfirmEmailBanner';
import messages from './messages';

const courseId = 'course-v1:edX+DemoX+Demo_Course';
let axiosMock;
let store;

function renderComponent() {
render(
<IntlProvider locale="en">
<ResponsiveContext.Provider value={{ width: 1280 }}>
<AppProvider store={store}>
<DiscussionsConfirmEmailBanner />
</AppProvider>
</ResponsiveContext.Provider>
</IntlProvider>,
);
}

describe('DiscussionsConfirmEmailBanner', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
store = initializeStore();
});

describe('render', () => {
it('does not show when email is verified', async () => {
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { isEmailVerified: true });
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
renderComponent();
expect(screen.queryByRole('alert')).toBeNull();
});

describe('when email is unverified', () => {
let resendEmailUrl;
beforeEach(async () => {
resendEmailUrl = `${getConfig().LMS_BASE_URL}/api/send_account_activation_email`;
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { isEmailVerified: false });
axiosMock.onPost(resendEmailUrl).reply(200);
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
renderComponent();
});

it('shows banner and confirm now button', async () => {
const banner = await screen.findByRole('alert');
expect(banner.textContent).toContain('Remember to confirm');
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
expect(confirmButton).toBeInTheDocument();
});

it('opens modal, closes banner, and calls resend email API when confirm now button is clicked', async () => {
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
await act(async () => {
fireEvent.click(confirmButton);
});
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(axiosMock.history.post).toHaveLength(1);
expect(axiosMock.history.post[0].url).toBe(resendEmailUrl);
});
});

it('shows modal header, body, image, and confirm email button and closes modal and banner on click', async () => {
const confirmButton = await screen.findByRole('button', { name: messages.confirmNowButton.defaultMessage });
await act(async () => {
fireEvent.click(confirmButton);
});
await waitFor(() => {
expect(screen.getByText(messages.confirmEmailModalHeader.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.confirmEmailModalBody.defaultMessage)).toBeInTheDocument();
expect(screen.getByRole('img', { name: messages.confirmEmailImageAlt.defaultMessage })).toBeInTheDocument();

const verifyButton = screen.getByRole('button', { name: messages.verifiedConfirmEmailButton.defaultMessage });
expect(verifyButton).toBeInTheDocument();
act(() => {
fireEvent.click(verifyButton);
});
});
await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});
});
});
});
8 changes: 7 additions & 1 deletion src/discussions/discussions-home/DiscussionsHome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const DiscussionsProductTour = lazy(() => import('../tours/DiscussionsProductTou
const DiscussionsRestrictionBanner = lazy(() => import('./DiscussionsRestrictionBanner'));
const DiscussionContent = lazy(() => import('./DiscussionContent'));
const DiscussionSidebar = lazy(() => import('./DiscussionSidebar'));
const DiscussionsConfirmEmailBanner = lazy(() => import('./DiscussionsConfirmEmailBanner'));

const DiscussionsHome = () => {
const location = useLocation();
Expand Down Expand Up @@ -81,7 +82,12 @@ const DiscussionsHome = () => {
return (
<Suspense fallback={(<Spinner />)}>
<DiscussionContext.Provider value={discussionContextValue}>
{!enableInContextSidebar && (<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />)}
{!enableInContextSidebar && (
<>
<DiscussionsConfirmEmailBanner />
<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />
</>
)}
<main className="container-fluid d-flex flex-column p-0 w-100 font-size" id="main" tabIndex="-1">
{!enableInContextSidebar && <CourseTabsNavigation />}
{(isEnrolled || !isUserLearner) && (
Expand Down
36 changes: 36 additions & 0 deletions src/discussions/discussions-home/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
confirmNowButton: {
id: 'discussions.confirmEmailBanner',
description: 'Button for sending confirm email and open modal',
defaultMessage: 'Confirm Now',
},
confirmEmailTextReminderBanner: {
id: 'discussions.confirmEmailTextReminderBanner',
description: 'Text for reminding user to confirm email',
defaultMessage: 'Remember to confirm your email so that you can keep posting! {confirmNowButton}.',
},
verifiedConfirmEmailButton: {
id: 'discussions.verifiedConfirmEmailButton',
description: 'Button for verified confirming email',
defaultMessage: 'I\'ve confirmed my email',
},
confirmEmailModalHeader: {
id: 'discussions.confirmEmailModalHeader',
description: 'title for confirming email modal',
defaultMessage: 'Confirm your email',
},
confirmEmailModalBody: {
id: 'discussions.confirmEmailModalBody',
description: 'text hint for confirming email modal',
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.',
},
confirmEmailImageAlt: {
id: 'discussions.confirmEmailImageAlt',
description: 'text alt confirm email image',
defaultMessage: 'confirm email background',
},
});

export default messages;
4 changes: 4 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,10 @@ th, td {
width: 16px !important;
}

.confirm-email-now-button {
text-decoration: underline !important;
}

@media only screen and (max-width: 367px) {

.discussion-comments h5,
Expand Down