diff --git a/src/assets/confirm-email.svg b/src/assets/confirm-email.svg new file mode 100644 index 000000000..cd7954cf0 --- /dev/null +++ b/src/assets/confirm-email.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/discussions/discussions-home/DiscussionsConfirmEmailBanner.jsx b/src/discussions/discussions-home/DiscussionsConfirmEmailBanner.jsx new file mode 100644 index 000000000..6a0ecb8e3 --- /dev/null +++ b/src/discussions/discussions-home/DiscussionsConfirmEmailBanner.jsx @@ -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 ( + <> + + {intl.formatMessage(messages.confirmEmailTextReminderBanner, { + confirmNowButton: ( + + ), + })} + + + {intl.formatMessage(messages.confirmEmailImageAlt)} + + )} + footerNode={( + + )} + > +

{intl.formatMessage(messages.confirmEmailModalHeader)}

+

{intl.formatMessage(messages.confirmEmailModalBody)}

+
+ + ); +}; + +export default DiscussionsConfirmEmailBanner; diff --git a/src/discussions/discussions-home/DiscussionsConfirmEmailBanner.test.jsx b/src/discussions/discussions-home/DiscussionsConfirmEmailBanner.test.jsx new file mode 100644 index 000000000..276eafc18 --- /dev/null +++ b/src/discussions/discussions-home/DiscussionsConfirmEmailBanner.test.jsx @@ -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( + + + + + + + , + ); +} + +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(); + }); + }); + }); + }); +}); diff --git a/src/discussions/discussions-home/DiscussionsHome.jsx b/src/discussions/discussions-home/DiscussionsHome.jsx index 1f57935fa..d09c3548c 100644 --- a/src/discussions/discussions-home/DiscussionsHome.jsx +++ b/src/discussions/discussions-home/DiscussionsHome.jsx @@ -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(); @@ -81,7 +82,12 @@ const DiscussionsHome = () => { return ( )}> - {!enableInContextSidebar && (
)} + {!enableInContextSidebar && ( + <> + +
+ + )}
{!enableInContextSidebar && } {(isEnrolled || !isUserLearner) && ( diff --git a/src/discussions/discussions-home/messages.js b/src/discussions/discussions-home/messages.js new file mode 100644 index 000000000..2985db37c --- /dev/null +++ b/src/discussions/discussions-home/messages.js @@ -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; diff --git a/src/index.scss b/src/index.scss index 6ee3813c6..3b0bd2f02 100755 --- a/src/index.scss +++ b/src/index.scss @@ -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,