From 776c6989bd15540da4ea54cd6bc277d58f63fe57 Mon Sep 17 00:00:00 2001 From: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com> Date: Tue, 2 Aug 2022 13:28:50 -0400 Subject: [PATCH] leangseu edx/confirm email banner (#4) * chore: working ui for confirm email * test: update unit test * chore: implement get values for state test * chore: update formatMessage test to support more than primitive variable --- public/confirm-email.svg | 76 +++++++++++++++++++ .../Banners/EntitlementBanner.test.jsx | 13 ---- .../EntitlementBanner.test.jsx.snap | 14 +--- .../ConfirmEmailBanner.scss | 3 + .../__snapshots__/index.test.jsx.snap | 72 ++++++++++++++++++ .../ConfirmEmailBanner/hooks.js | 44 +++++++++++ .../ConfirmEmailBanner/hooks.test.js | 76 +++++++++++++++++++ .../ConfirmEmailBanner/index.jsx | 66 ++++++++++++++++ .../ConfirmEmailBanner/index.test.jsx | 35 +++++++++ .../ConfirmEmailBanner/messages.js | 36 +++++++++ .../LearnerDashboardHeader/index.jsx | 2 + src/data/redux/thunkActions/app.js | 4 + src/setupTest.jsx | 3 + src/testUtils.js | 9 +++ 14 files changed, 429 insertions(+), 24 deletions(-) create mode 100644 public/confirm-email.svg create mode 100644 src/containers/LearnerDashboardHeader/ConfirmEmailBanner/ConfirmEmailBanner.scss create mode 100644 src/containers/LearnerDashboardHeader/ConfirmEmailBanner/__snapshots__/index.test.jsx.snap create mode 100644 src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js create mode 100644 src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js create mode 100644 src/containers/LearnerDashboardHeader/ConfirmEmailBanner/index.jsx create mode 100644 src/containers/LearnerDashboardHeader/ConfirmEmailBanner/index.test.jsx create mode 100644 src/containers/LearnerDashboardHeader/ConfirmEmailBanner/messages.js diff --git a/public/confirm-email.svg b/public/confirm-email.svg new file mode 100644 index 0000000..cd7954c --- /dev/null +++ b/public/confirm-email.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx b/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx index b805750..991c5d5 100644 --- a/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx +++ b/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx @@ -1,13 +1,9 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { useIntl } from '@edx/frontend-platform/i18n'; import { hooks as appHooks } from 'data/redux'; import EntitlementBanner from './EntitlementBanner'; -jest.mock('@edx/frontend-platform/i18n', () => ({ - useIntl: jest.fn(), -})); jest.mock('components/Banner', () => 'Banner'); jest.mock('data/redux', () => ({ hooks: { @@ -37,15 +33,6 @@ const render = (overrides = {}) => { }; describe('EntitlementBanner', () => { - beforeEach(() => { - useIntl.mockReturnValue({ - formatDate: (date) => date, - formatMessage: (message, values) =>
, - }); - }); - afterEach(() => { - jest.clearAllMocks(); - }); it('initializes data with course number from entitlements', () => { render(); expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber); diff --git a/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap b/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap index cad1507..8c36286 100644 --- a/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap +++ b/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap @@ -2,7 +2,7 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = ` -
-
+ select a session , } } @@ -38,7 +30,7 @@ exports[`EntitlementBanner snapshot: no sessions available 1`] = ` -
+ + + Confirm Now + , + } + } + /> + + + I've confirmed my email + + } + hasCloseButton={false} + heroNode={ + + confirm email background + + } + isOpen={[MockFunction showConfirmModal]} + onClose={[MockFunction closeConfirmModal]} + title="" + > +

+ Confirm your email +

+

+ We've sent you an email to verify your acccount. Please check your inbox and click on the big red button to confirm and keep learning. +

+
+ +`; + +exports[`ConfirmEmailBanner snapshot do not show on already verified 1`] = `""`; diff --git a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js new file mode 100644 index 0000000..fc8dc5d --- /dev/null +++ b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js @@ -0,0 +1,44 @@ +import React from 'react'; + +import { StrictDict } from 'utils'; +import { hooks as appHooks, thunkActions } from 'data/redux'; + +import { useDispatch } from 'react-redux'; + +import * as module from './hooks'; + +export const state = StrictDict({ + showPageBanner: (val) => React.useState(val), // eslint-disable-line + showConfirmModal: (val) => React.useState(val), // eslint-disable-line +}); + +export const useConfirmEmailBannerData = () => { + const dispatch = useDispatch(); + const { isNeeded } = appHooks.useEmailConfirmationData(); + const [showPageBanner, setShowPageBanner] = module.state.showPageBanner(isNeeded); + const [showConfirmModal, setShowConfirmModal] = module.state.showConfirmModal(false); + const closePageBanner = () => setShowPageBanner(false); + const closeConfirmModal = () => setShowConfirmModal(false); + const openConfirmModal = () => setShowConfirmModal(true); + + const openConfirmModalButtonClick = () => { + dispatch(thunkActions.app.sendConfirmEmail()); + openConfirmModal(); + }; + + const userConfirmEmailButtonClick = () => { + closeConfirmModal(); + closePageBanner(); + }; + return { + isNeeded, + showPageBanner, + closePageBanner, + showConfirmModal, + closeConfirmModal, + openConfirmModalButtonClick, + userConfirmEmailButtonClick, + }; +}; + +export default useConfirmEmailBannerData; diff --git a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js new file mode 100644 index 0000000..50e1047 --- /dev/null +++ b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js @@ -0,0 +1,76 @@ +import { MockUseState } from 'testUtils'; +import { hooks as appHooks, thunkActions } from 'data/redux'; + +import * as hooks from './hooks'; + +jest.mock('data/redux', () => ({ + hooks: { + useEmailConfirmationData: jest.fn(), + }, + thunkActions: { + app: { + sendConfirmEmail: jest.fn(), + }, + }, +})); + +const emailConfirmation = { + isNeeded: true, +}; + +const state = new MockUseState(hooks); + +describe('ConfirmEmailBanner hooks', () => { + let out; + describe('state values', () => { + state.testGetter(state.keys.showPageBanner); + state.testGetter(state.keys.showConfirmModal); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('useEmailConfirmationData', () => { + beforeEach(() => state.mock()); + afterEach(state.restore); + + test('show page banner on unverified email', () => { + appHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation }); + out = hooks.useConfirmEmailBannerData(); + expect(out.isNeeded).toEqual(emailConfirmation.isNeeded); + appHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false }); + }); + + test('hide page banner on verified email', () => { + appHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false }); + out = hooks.useConfirmEmailBannerData(); + expect(out.isNeeded).toEqual(false); + }); + }); + + describe('behavior', () => { + beforeEach(() => { + state.mock(); + appHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation }); + out = hooks.useConfirmEmailBannerData(); + }); + afterEach(state.restore); + test('closePageBanner', () => { + out.closePageBanner(); + expect(state.values.showPageBanner).toEqual(false); + }); + test('closeConfirmModal', () => { + out.closeConfirmModal(); + expect(state.values.showConfirmModal).toEqual(false); + }); + test('openConfirmModalButtonClick', () => { + out.openConfirmModalButtonClick(); + expect(state.values.showConfirmModal).toEqual(true); + expect(thunkActions.app.sendConfirmEmail).toBeCalled(); + }); + test('userConfirmEmailButtonClick', () => { + out.userConfirmEmailButtonClick(); + expect(state.values.showConfirmModal).toEqual(false); + expect(state.values.showPageBanner).toEqual(false); + }); + }); +}); diff --git a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/index.jsx b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/index.jsx new file mode 100644 index 0000000..1266a70 --- /dev/null +++ b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/index.jsx @@ -0,0 +1,66 @@ +/* eslint-disable max-len */ +import React from 'react'; +import { + PageBanner, ModalDialog, MarketingModal, Button, +} from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; +import './ConfirmEmailBanner.scss'; +import useConfirmEmailBannerData from './hooks'; + +export const ConfirmEmailBanner = () => { + const { + isNeeded, + showConfirmModal, + showPageBanner, + closePageBanner, + closeConfirmModal, + openConfirmModalButtonClick, + userConfirmEmailButtonClick, + } = useConfirmEmailBannerData(); + const { formatMessage } = useIntl(); + + if (!isNeeded) { return null; } + + return ( + <> + + {formatMessage(messages.confirmEmailTextReminderBanner, { + confirmNowButton: ( + + ), + })} + + + {formatMessage(messages.confirmEmailImageAlt)} + + )} + footerNode={( + + )} + > +

{formatMessage(messages.confirmEmailModalHeader)}

+

{formatMessage(messages.confirmEmailModalBody)}

+
+ + ); +}; +ConfirmEmailBanner.propTypes = {}; + +export default ConfirmEmailBanner; diff --git a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/index.test.jsx b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/index.test.jsx new file mode 100644 index 0000000..600266e --- /dev/null +++ b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/index.test.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import hooks from './hooks'; +import ConfirmEmailBanner from '.'; + +jest.mock('./hooks', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const hookProps = { + isNeeded: true, + showPageBanner: jest.fn().mockName('showPageBanner'), + closePageBanner: jest.fn().mockName('closePageBanner'), + showConfirmModal: jest.fn().mockName('showConfirmModal'), + closeConfirmModal: jest.fn().mockName('closeConfirmModal'), + openConfirmModalButtonClick: jest.fn().mockName('openConfirmModalButtonClick'), + userConfirmEmailButtonClick: jest.fn().mockName('userConfirmEmailButtonClick'), +}; + +describe('ConfirmEmailBanner', () => { + describe('snapshot', () => { + test('do not show on already verified', () => { + hooks.mockReturnValueOnce({ ...hookProps, isNeeded: false }); + const el = shallow(); + expect(el).toMatchSnapshot(); + }); + test('Show on unverified', () => { + hooks.mockReturnValueOnce({ ...hookProps }); + const el = shallow(); + expect(el).toMatchSnapshot(); + }); + }); +}); diff --git a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/messages.js b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/messages.js new file mode 100644 index 0000000..5260f47 --- /dev/null +++ b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/messages.js @@ -0,0 +1,36 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + confirmNowButton: { + id: 'leanerDashboard.confirmEmailBanner', + description: 'Button for sending confirm email and open modal', + defaultMessage: 'Confirm Now', + }, + confirmEmailTextReminderBanner: { + id: 'leanerDashboard.confirmEmailTextReminderBanner', + description: 'Text for reminding user to confirm email', + defaultMessage: 'Remember to confirm your email so that you can keep learning on edX! {confirmNowButton}.', + }, + verifiedConfirmEmailButton: { + id: 'leanerDashboard.verifiedConfirmEmailButton', + description: 'Button for verified confirming email', + defaultMessage: 'I\'ve confirmed my email', + }, + confirmEmailModalHeader: { + id: 'leanerDashboard.confirmEmailModalHeader', + description: 'title for confirming email modal', + defaultMessage: 'Confirm your email', + }, + confirmEmailModalBody: { + id: 'leanerDashboard.confirmEmailModalBody', + description: 'text hint for confirming email modal', + defaultMessage: 'We\'ve sent you an email to verify your acccount. Please check your inbox and click on the big red button to confirm and keep learning.', + }, + confirmEmailImageAlt: { + id: 'leanerDashboard.confirmEmailImageAlt', + description: 'text alt confirm email image', + defaultMessage: 'confirm email background', + }, +}); + +export default messages; diff --git a/src/containers/LearnerDashboardHeader/index.jsx b/src/containers/LearnerDashboardHeader/index.jsx index 4dba2c2..01c6cb9 100644 --- a/src/containers/LearnerDashboardHeader/index.jsx +++ b/src/containers/LearnerDashboardHeader/index.jsx @@ -8,12 +8,14 @@ import { Button } from '@edx/paragon'; import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; import GreetingBanner from './GreetingBanner'; import messages from './messages'; +import ConfirmEmailBanner from './ConfirmEmailBanner'; export const LearnerDashboardHeader = () => { const { authenticatedUser } = useContext(AppContext); const { formatMessage } = useIntl(); return (
+