From e915dc505e35cb0331c4419582d6d85ed5919835 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Thu, 28 Apr 2022 23:41:18 +0300 Subject: [PATCH 1/8] feat: email verification todo: resend email logic --- pages/{index.js => index.tsx} | 0 src/Me/Me.tsx | 9 ++++++++- src/api/index.ts | 5 +++-- src/components/layouts/App/App.js | 26 +++++++++++++++++++++++++ src/context/userContext/UserContext.tsx | 14 ++++++++++--- src/hooks/useRoutes.ts | 2 +- src/types/models.d.ts | 1 + src/utils/auth.js | 1 + 8 files changed, 51 insertions(+), 7 deletions(-) rename pages/{index.js => index.tsx} (100%) diff --git a/pages/index.js b/pages/index.tsx similarity index 100% rename from pages/index.js rename to pages/index.tsx diff --git a/src/Me/Me.tsx b/src/Me/Me.tsx index b8da5c47..46571f83 100644 --- a/src/Me/Me.tsx +++ b/src/Me/Me.tsx @@ -13,10 +13,12 @@ import { desktop } from './styles/shared/devices'; import { isSsr } from '../helpers/ssr'; import { useUser } from '../context/userContext/UserContext'; import { useAuth } from '../context/authContext/AuthContext'; +import { useRoutes } from '../hooks/useRoutes'; const Me = (props: any) => { const { children, title } = props; - const { pathname } = useRouter(); + const { pathname, push } = useRouter(); + const routes = useRoutes(); const { currentUser, isLoading } = useUser(); const auth = useAuth(); @@ -34,6 +36,11 @@ const Me = (props: any) => { return null; } + if (!currentUser.email_verified) { + push(routes.root.get()); + return

Email not verified, redirecting...

; + } + return ( <> diff --git a/src/api/index.ts b/src/api/index.ts index 43737ac3..d68ee007 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -6,6 +6,7 @@ import partition from 'lodash/partition'; import * as Sentry from '@sentry/browser'; import { Application, Mentor, User } from '../types/models'; import { setVisitor } from '../utils/tawk'; +import Auth from '../utils/auth'; type RequestMethod = 'POST' | 'GET' | 'PUT' | 'DELETE'; type ErrorResponse = { @@ -32,9 +33,9 @@ let currentUser: User | undefined; export default class ApiService { mentorsPromise: Promise | null = null - auth: any + auth: Auth; - constructor(auth: any) { + constructor(auth: Auth) { this.auth = auth } diff --git a/src/components/layouts/App/App.js b/src/components/layouts/App/App.js index bcaa4744..e21c2c3c 100644 --- a/src/components/layouts/App/App.js +++ b/src/components/layouts/App/App.js @@ -11,6 +11,7 @@ import { ActionsHandler } from './ActionsHandler'; import { desktop, mobile } from '../../../Me/styles/shared/devices'; import { Sidebar } from '../../Sidebar/Sidebar'; import { useMentors } from '../../../context/mentorsContext/MentorsContext'; +import { useUser } from '../../../context/userContext/UserContext'; const App = (props) => { const { children } = props; @@ -22,6 +23,7 @@ const App = (props) => { onClose: null, }); const { mentors } = useMentors(); + const {isEmailVerifed} = useUser(); useEffect(() => { if (process.env.REACT_APP_MAINTENANCE_MESSAGE) { @@ -38,11 +40,35 @@ const App = (props) => { } }, []); + useEffect(() => { + if (isEmailVerifed === false) { + showVerifyEmailModal(); + } + }, [isEmailVerifed]); + useEffect( () => setWindowTitle({ tag, country, name, language }), [tag, country, name, language] ); + const showVerifyEmailModal = () => { + throw new Error('Not implemented'); + setModal({ + title: 'Verify your email', + content: ( +
+

+ Please verify your email address to continue using the platform. +

+

+ Resend verification email +

+
+ ), + onClose: () => setModal({}), + }); + } + const handleModal = (title, content, onClose) => { setModal({ title, diff --git a/src/context/userContext/UserContext.tsx b/src/context/userContext/UserContext.tsx index 8ced6e2e..36ba174c 100644 --- a/src/context/userContext/UserContext.tsx +++ b/src/context/userContext/UserContext.tsx @@ -8,6 +8,7 @@ type UserProviderContext = { isMentor: boolean; isLoading: boolean; currentUser?: User; + isEmailVerifed?: boolean; isAuthenticated: boolean; updateCurrentUser(user: User): void; logout(): void; @@ -20,6 +21,7 @@ const UserContext = React.createContext( export const UserProvider: FC = ({ children }) => { const [isLoading, setIsloading] = useState(true); const [currentUser, updateCurrentUser] = useState(); + const [isEmailVerifed, setIsEmailVerified] = useState(); const auth = useAuth(); const api = useApi(); const isAuthenticated = auth.isAuthenticated(); @@ -31,10 +33,15 @@ export const UserProvider: FC = ({ children }) => { }; useEffect(() => { - api.getCurrentUser().then((user) => { - updateCurrentUser(user); + async function getCurrentUser() { + const user = await api.getCurrentUser(); setIsloading(false); - }); + setIsEmailVerified(user.email_verified); + if (user.email_verified) { + updateCurrentUser(user); + } + } + getCurrentUser(); }, [api]); return ( @@ -44,6 +51,7 @@ export const UserProvider: FC = ({ children }) => { isMentor, isLoading, currentUser, + isEmailVerifed, isAuthenticated, logout, updateCurrentUser, diff --git a/src/hooks/useRoutes.ts b/src/hooks/useRoutes.ts index 7d2e9834..dd980aaf 100644 --- a/src/hooks/useRoutes.ts +++ b/src/hooks/useRoutes.ts @@ -11,7 +11,7 @@ export const useRoutes = () => { return { root: { - get: () => getUrlWithFilterParams('/'), + get: () => getUrlWithFilterParams('/') }, user: { get: (userOrUsreId: User | string) => diff --git a/src/types/models.d.ts b/src/types/models.d.ts index 3b9d54b2..86477665 100644 --- a/src/types/models.d.ts +++ b/src/types/models.d.ts @@ -17,6 +17,7 @@ export type User = BaseDBObject & { name: string; title: string; email: string; + email_verified: boolean; tags: string[]; avatar?: string; country: Country; diff --git a/src/utils/auth.js b/src/utils/auth.js index 700c5602..d4b5f6c4 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -45,6 +45,7 @@ class Auth { appState: { origin: isMentorIntent ? 'mentor' : 'user', }, + scope: 'email', redirectUri: redirectTo ? `${this.redirectUri}?redirectTo=${redirectTo}` : window.location.href, From 4f5240d1deb5824b831c2ddaac83ed9daa47ee1e Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Fri, 29 Apr 2022 10:07:08 +0300 Subject: [PATCH 2/8] show modal + mask email todo: resend verification email --- src/components/layouts/App/App.js | 14 +++++++------- src/context/userContext/UserContext.tsx | 23 +++++++++++++++++++---- src/utils/maskSansitiveString.ts | 9 +++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/utils/maskSansitiveString.ts diff --git a/src/components/layouts/App/App.js b/src/components/layouts/App/App.js index e21c2c3c..15d08db9 100644 --- a/src/components/layouts/App/App.js +++ b/src/components/layouts/App/App.js @@ -12,6 +12,7 @@ import { desktop, mobile } from '../../../Me/styles/shared/devices'; import { Sidebar } from '../../Sidebar/Sidebar'; import { useMentors } from '../../../context/mentorsContext/MentorsContext'; import { useUser } from '../../../context/userContext/UserContext'; +import { maskEmail } from '../../../utils/maskSansitiveString'; const App = (props) => { const { children } = props; @@ -23,7 +24,7 @@ const App = (props) => { onClose: null, }); const { mentors } = useMentors(); - const {isEmailVerifed} = useUser(); + const {emailVerifedInfo} = useUser(); useEffect(() => { if (process.env.REACT_APP_MAINTENANCE_MESSAGE) { @@ -41,24 +42,23 @@ const App = (props) => { }, []); useEffect(() => { - if (isEmailVerifed === false) { - showVerifyEmailModal(); + if (emailVerifedInfo?.isVerified === false) { + showVerifyEmailModal(emailVerifedInfo.email); } - }, [isEmailVerifed]); + }, [emailVerifedInfo]); useEffect( () => setWindowTitle({ tag, country, name, language }), [tag, country, name, language] ); - const showVerifyEmailModal = () => { - throw new Error('Not implemented'); + const showVerifyEmailModal = (email) => { setModal({ title: 'Verify your email', content: (

- Please verify your email address to continue using the platform. + Please verify your email address ({maskEmail(email)}) to continue using the platform.

Resend verification email diff --git a/src/context/userContext/UserContext.tsx b/src/context/userContext/UserContext.tsx index 36ba174c..68b314db 100644 --- a/src/context/userContext/UserContext.tsx +++ b/src/context/userContext/UserContext.tsx @@ -3,12 +3,19 @@ import { User } from '../../types/models'; import { useAuth } from '../authContext/AuthContext'; import { useApi } from '../apiContext/ApiContext'; +type EmailNotVerifiedInfo = { + isVerified: true; +} | { + isVerified: false; + email: string; +} + type UserProviderContext = { isAdmin: boolean; isMentor: boolean; isLoading: boolean; currentUser?: User; - isEmailVerifed?: boolean; + emailVerifedInfo?: EmailNotVerifiedInfo; isAuthenticated: boolean; updateCurrentUser(user: User): void; logout(): void; @@ -21,7 +28,7 @@ const UserContext = React.createContext( export const UserProvider: FC = ({ children }) => { const [isLoading, setIsloading] = useState(true); const [currentUser, updateCurrentUser] = useState(); - const [isEmailVerifed, setIsEmailVerified] = useState(); + const [emailVerifedInfo, setEmailVerifedInfo] = useState(); const auth = useAuth(); const api = useApi(); const isAuthenticated = auth.isAuthenticated(); @@ -36,7 +43,15 @@ export const UserProvider: FC = ({ children }) => { async function getCurrentUser() { const user = await api.getCurrentUser(); setIsloading(false); - setIsEmailVerified(user.email_verified); + if (!user) { + return; + } + + setEmailVerifedInfo({ + isVerified: user.email_verified, + email: user.email, + }); + if (user.email_verified) { updateCurrentUser(user); } @@ -51,7 +66,7 @@ export const UserProvider: FC = ({ children }) => { isMentor, isLoading, currentUser, - isEmailVerifed, + emailVerifedInfo, isAuthenticated, logout, updateCurrentUser, diff --git a/src/utils/maskSansitiveString.ts b/src/utils/maskSansitiveString.ts new file mode 100644 index 00000000..f6dcec1a --- /dev/null +++ b/src/utils/maskSansitiveString.ts @@ -0,0 +1,9 @@ +const replaceWithAsterisk = (str: string) => { + return str.replace(/./g, '*'); +} + +export const maskEmail = (email: string) => { + return email.replace(/(.)(.*)(.@.)(.*)(.\..*)/, (...[, g1l1, g1Rest, at, g2Rest, sufix]) => { + return `${g1l1}${replaceWithAsterisk(g1Rest)}${at}${replaceWithAsterisk(g2Rest)}${sufix}`; + }); +} From 1b75348f21c8fe85b232f3a88a55bfe83b051cfa Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Mon, 2 May 2022 02:05:27 +0300 Subject: [PATCH 3/8] complete verification modal todo: handle verification callback - success / error --- src/api/index.ts | 4 ++ src/components/Modal/Modal.js | 4 +- src/components/layouts/App/App.js | 41 ++++++++-------- .../layouts/App/VerificationModal.tsx | 47 +++++++++++++++++++ src/utils/auth.js | 2 +- 5 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 src/components/layouts/App/VerificationModal.tsx diff --git a/src/api/index.ts b/src/api/index.ts index d68ee007..3645dfb2 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -387,4 +387,8 @@ export default class ApiService { return false; } + + resendVerificationEmail = async () => { + return this.makeApiCall(`${paths.USERS}/verify`, null, 'POST'); + } } diff --git a/src/components/Modal/Modal.js b/src/components/Modal/Modal.js index 918c095a..976436e8 100644 --- a/src/components/Modal/Modal.js +++ b/src/components/Modal/Modal.js @@ -3,12 +3,12 @@ import classNames from 'classnames'; export default class Modal extends Component { state = { - isActive: false, + isActive: this.props.isActive ?? false, }; handleOpen = (children) => { this.setState({ - isActive: true, + isActive: !!children, children, }); }; diff --git a/src/components/layouts/App/App.js b/src/components/layouts/App/App.js index 15d08db9..e4f03dbd 100644 --- a/src/components/layouts/App/App.js +++ b/src/components/layouts/App/App.js @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components/macro'; import { toast, ToastContainer } from 'react-toastify'; @@ -12,7 +12,7 @@ import { desktop, mobile } from '../../../Me/styles/shared/devices'; import { Sidebar } from '../../Sidebar/Sidebar'; import { useMentors } from '../../../context/mentorsContext/MentorsContext'; import { useUser } from '../../../context/userContext/UserContext'; -import { maskEmail } from '../../../utils/maskSansitiveString'; +import { VerificationModal } from './VerificationModal'; const App = (props) => { const { children } = props; @@ -24,7 +24,23 @@ const App = (props) => { onClose: null, }); const { mentors } = useMentors(); - const {emailVerifedInfo} = useUser(); + const { emailVerifedInfo } = useUser(); + const closeModal = useCallback(() => setModal({}), []); + + const showVerifyEmailModal = useCallback(() => { + setModal({ + title: 'Verify your email', + content: ( + { + toast.success('Email verified successfully'); + closeModal(); + }} + /> + ), + onClose: closeModal, + }); + }, [closeModal]); useEffect(() => { if (process.env.REACT_APP_MAINTENANCE_MESSAGE) { @@ -45,30 +61,13 @@ const App = (props) => { if (emailVerifedInfo?.isVerified === false) { showVerifyEmailModal(emailVerifedInfo.email); } - }, [emailVerifedInfo]); + }, [emailVerifedInfo, showVerifyEmailModal]); useEffect( () => setWindowTitle({ tag, country, name, language }), [tag, country, name, language] ); - const showVerifyEmailModal = (email) => { - setModal({ - title: 'Verify your email', - content: ( -

-

- Please verify your email address ({maskEmail(email)}) to continue using the platform. -

-

- Resend verification email -

-
- ), - onClose: () => setModal({}), - }); - } - const handleModal = (title, content, onClose) => { setModal({ title, diff --git a/src/components/layouts/App/VerificationModal.tsx b/src/components/layouts/App/VerificationModal.tsx new file mode 100644 index 00000000..17723b90 --- /dev/null +++ b/src/components/layouts/App/VerificationModal.tsx @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import { useApi } from '../../../context/apiContext/ApiContext'; +import { useUser } from '../../../context/userContext/UserContext'; +import Button from '../../../Me/components/Button'; +import { maskEmail } from '../../../utils/maskSansitiveString'; + +type VerificationModalProps = { + onSuccess: () => void; + email: string; +}; + +export const VerificationModal = ({ onSuccess }: VerificationModalProps) => { + const [loading, setLoading] = useState(false); + const { emailVerifedInfo } = useUser(); + const api = useApi(); + + if (emailVerifedInfo.isVerified === true) { + // eslint-disable-next-line no-console + console.warn('email is verified'); + return; + } + + const send = async () => { + setLoading(true); + try { + const result = await api.resendVerificationEmail(); + if (result.success) { + onSuccess(); + } + } catch {} + setLoading(false); + }; + + return ( + <> +

+ Please verify your email address ({maskEmail(emailVerifedInfo.email)}) + to continue using the platform. +

+

+ +

+ + ); +}; diff --git a/src/utils/auth.js b/src/utils/auth.js index d4b5f6c4..65457524 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -45,7 +45,7 @@ class Auth { appState: { origin: isMentorIntent ? 'mentor' : 'user', }, - scope: 'email', + scope: 'openid email', redirectUri: redirectTo ? `${this.redirectUri}?redirectTo=${redirectTo}` : window.location.href, From 1e3799789fb4c7a16e36b25bce45b5f161c002ef Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Mon, 2 May 2022 09:57:55 +0300 Subject: [PATCH 4/8] handle renew-session error, re-style toast --- package.json | 1 + src/context/authContext/AuthContext.tsx | 22 +++++++++++++++++---- src/index.css | 26 +++++++++++++++++++++++-- src/utils/auth.js | 5 ++--- yarn.lock | 5 +++++ 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 0f6caa3d..996d275d 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@testing-library/jest-dom": "^5.1.1", "@testing-library/react": "^11.2.3", "@testing-library/user-event": "^12.6.3", + "@types/auth0-js": "^9.14.6", "@types/classnames": "^2.3.1", "@types/jest": "^26.0.23", "@types/lodash": "^4.14.170", diff --git a/src/context/authContext/AuthContext.tsx b/src/context/authContext/AuthContext.tsx index e09785ec..8977bef0 100644 --- a/src/context/authContext/AuthContext.tsx +++ b/src/context/authContext/AuthContext.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, FC, useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { isSsr } from '../../helpers/ssr'; import Auth from '../../utils/auth'; @@ -7,12 +8,25 @@ const auth = new Auth(); export const AuthProvider: FC = (props: any) => { const { children } = props; - const [isLoading, setIsLoading] = useState(!isSsr() /* ssr doesn't need loader */); + const [isLoading, setIsLoading] = useState( + !isSsr() /* ssr doesn't need loader */ + ); useEffect(() => { - auth.renewSession().finally(() => { - setIsLoading(false); - }); + auth + .renewSession() + .catch((e) => { + toast.error( + <> +
Something went wrong, please login again
+
If the problem persists, please contact us.
+
Error: {e}
+ + ); + }) + .finally(() => { + setIsLoading(false); + }); }, []); if (isLoading) { diff --git a/src/index.css b/src/index.css index bf3c31b9..0511e406 100755 --- a/src/index.css +++ b/src/index.css @@ -99,12 +99,34 @@ code { } /* #region toast overrides */ +.Toastify__toast-container { + width: auto; + min-width: 320px; + max-width: 480px; +} + +.Toastify__toast { + gap: 5px; + font-size: 16px; + line-height: 1.4; + border: 1px solid; + background: #fff; + border-radius: 5px; + font-family: inherit; + color: rgb(var(--toast-color)); + box-shadow: inset 0 0 0 1000px rgb(var(--toast-color) / 0.2); +} + .Toastify__toast--error { - background: var(--error-background); + --toast-color: 220 53 69; } .Toastify__toast--success { - background: var(--success-background); + --toast-color: 40 167 69; +} + +.Toastify__close-button { + color: inherit; } /* #endregion */ diff --git a/src/utils/auth.js b/src/utils/auth.js index 65457524..8951a54f 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -23,7 +23,7 @@ class Auth { clientID: this.clientId, redirectUri: this.redirectUri, responseType: 'token id_token', - scope: 'openid', + scope: 'openid email', }); this.loadSession(); @@ -45,7 +45,6 @@ class Auth { appState: { origin: isMentorIntent ? 'mentor' : 'user', }, - scope: 'openid email', redirectUri: redirectTo ? `${this.redirectUri}?redirectTo=${redirectTo}` : window.location.href, @@ -145,7 +144,7 @@ class Auth { } else if (!this.isAuthenticated()) { this.auth0.checkSession({}, (err, authResult) => { if (err) { - reject(err); + return reject(err); } if (authResult && authResult.accessToken && authResult.idToken) { this.setSession(authResult); diff --git a/yarn.lock b/yarn.lock index 96c63a4b..b4f96f7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2699,6 +2699,11 @@ version "4.2.2" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" +"@types/auth0-js@^9.14.6": + version "9.14.6" + resolved "https://registry.yarnpkg.com/@types/auth0-js/-/auth0-js-9.14.6.tgz#a1b0cdaf1c5a7062480455d9b255326bdc748117" + integrity sha512-dQrWP1G5PO5onJcDdto803OKvw1mplNBoRTdMJzRemfLyKSMAShiVBVD12jZ8VbB1RRDlg6R2eheEH/sxv1ZtQ== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.16" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.16.tgz#bc12c74b7d65e82d29876b5d0baf5c625ac58702" From 5cb98f57027202ede9995cb037f9dec6b8658f97 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Wed, 4 May 2022 11:38:47 +0300 Subject: [PATCH 5/8] complete the flow & improve verification modal design --- src/api/index.ts | 5 +++ src/components/Header/Header.js | 6 +-- src/components/MemberArea/MemberArea.js | 14 ++++--- src/components/layouts/App/App.js | 2 +- .../layouts/App/VerificationModal.tsx | 36 ++++++++++++----- src/context/apiContext/ApiContext.tsx | 11 ++++-- src/context/authContext/AuthContext.tsx | 39 +++++++++++++++++-- src/context/userContext/UserContext.tsx | 8 +++- src/utils/auth.js | 24 ++++++++---- 9 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 3645dfb2..e8e9dc50 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -115,6 +115,11 @@ export default class ApiService { clearCurrentUser = () => { currentUser = undefined; + ApiService.clearCurrentUserFromStorage(); + } + + // because we need to call it from authContext which doesn't have access to ApiService + static clearCurrentUserFromStorage = () => { localStorage.removeItem(USER_LOCAL_KEY); } diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 1ce3fc24..3095a9a1 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -1,4 +1,4 @@ -import React, { useState, useContext } from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import OffCanvas from 'react-aria-offcanvas'; import Modal from '../Modal/Modal'; @@ -8,7 +8,7 @@ import Logo from '../Logo'; import Title from '../SiteTitle'; import Navigation from '../Navigation/Navigation'; import MobileNavigation from '../MobileNavigation/MobileNavigation'; -import { AuthContext } from '../../context/authContext/AuthContext'; +import { useAuth } from '../../context/authContext/AuthContext'; import { useDeviceType } from '../../hooks/useDeviceType'; function Header() { @@ -19,7 +19,7 @@ function Header() { }); const [isOpen, setIsOpen] = useState(false); const { isDesktop } = useDeviceType(); - const auth = useContext(AuthContext); + const auth = useAuth(); const authenticated = auth.isAuthenticated(); const handleModal = ({ title, content, onClose }) => { diff --git a/src/components/MemberArea/MemberArea.js b/src/components/MemberArea/MemberArea.js index d3edb766..91155ee2 100644 --- a/src/components/MemberArea/MemberArea.js +++ b/src/components/MemberArea/MemberArea.js @@ -18,6 +18,7 @@ function MemberArea({ onOpenModal }) { const [isMemberMenuOpen, setIsMemberMenuOpen] = useState(false); const { currentUser, isMentor, isAdmin, isAuthenticated, logout } = useUser(); const api = useApi(); + const user = useUser(); const auth = useAuth(); const openBecomeMentor = useCallback( () => onOpenModal('Edit Your Profile', ), @@ -51,7 +52,8 @@ function MemberArea({ onOpenModal }) { - currentUser && setIsMemberMenuOpen(!isMemberMenuOpen) + (currentUser || user.isNotYetVerified) && + setIsMemberMenuOpen(!isMemberMenuOpen) } > {currentUser ? ( @@ -70,10 +72,12 @@ function MemberArea({ onOpenModal }) { Open pending applications )} - - Manage Account - - {!isMentor && ( + {!user.isNotYetVerified && ( + + Manage Account + + )} + {!isMentor && !user.isNotYetVerified && ( Become a mentor diff --git a/src/components/layouts/App/App.js b/src/components/layouts/App/App.js index e4f03dbd..f7be2fcd 100644 --- a/src/components/layouts/App/App.js +++ b/src/components/layouts/App/App.js @@ -33,7 +33,7 @@ const App = (props) => { content: ( { - toast.success('Email verified successfully'); + toast.success('We just sent you the verification email'); closeModal(); }} /> diff --git a/src/components/layouts/App/VerificationModal.tsx b/src/components/layouts/App/VerificationModal.tsx index 17723b90..7321f4c7 100644 --- a/src/components/layouts/App/VerificationModal.tsx +++ b/src/components/layouts/App/VerificationModal.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import styled from 'styled-components/macro'; import { useApi } from '../../../context/apiContext/ApiContext'; import { useUser } from '../../../context/userContext/UserContext'; import Button from '../../../Me/components/Button'; @@ -9,6 +10,12 @@ type VerificationModalProps = { email: string; }; +const ModalText = styled.p` + text-align: center; + font-size: 16px; + line-height: 1.5; +`; + export const VerificationModal = ({ onSuccess }: VerificationModalProps) => { const [loading, setLoading] = useState(false); const { emailVerifedInfo } = useUser(); @@ -33,15 +40,26 @@ export const VerificationModal = ({ onSuccess }: VerificationModalProps) => { return ( <> -

- Please verify your email address ({maskEmail(emailVerifedInfo.email)}) - to continue using the platform. -

-

- -

+ + Psst, we believe that you are who you say you are. +
+ Just to make sure, we need you to verify your email. +

Recognize {maskEmail(emailVerifedInfo.email)}?

+ {emailVerifedInfo.isRegisteredRecently ? ( + <> + This is the address we sent a verification email to. +
+ Can't find it? Hit the button + + ) : ( + <>Hit the button to send a verification email right to your inbox + )} +

+ +

+
); }; diff --git a/src/context/apiContext/ApiContext.tsx b/src/context/apiContext/ApiContext.tsx index fe26657c..a99f4731 100644 --- a/src/context/apiContext/ApiContext.tsx +++ b/src/context/apiContext/ApiContext.tsx @@ -5,13 +5,16 @@ import ApiService from '../../api'; export const ApiContext = createContext(null); export const ApiProvider: FC = (props: any) => { - const { children } = props - const auth = useContext(AuthContext) - const api = useMemo(() => new ApiService(auth), [auth]) ; - return {children} + const { children } = props; + const auth = useContext(AuthContext); + const api = useMemo(() => new ApiService(auth), [auth]); + return {children}; }; export function useApi(): ApiService { const api = useContext(ApiContext); + if (!api) { + throw new Error(`"useApi" has to be called inside ApiProvider`); + } return api; } diff --git a/src/context/authContext/AuthContext.tsx b/src/context/authContext/AuthContext.tsx index 8977bef0..32f52bca 100644 --- a/src/context/authContext/AuthContext.tsx +++ b/src/context/authContext/AuthContext.tsx @@ -1,4 +1,5 @@ -import { createContext, useContext, FC, useEffect, useState } from 'react'; +import { NextRouter, useRouter } from 'next/router'; +import { createContext, useContext, FC, useEffect, useState, useCallback, useRef } from 'react'; import { toast } from 'react-toastify'; import { isSsr } from '../../helpers/ssr'; import Auth from '../../utils/auth'; @@ -8,11 +9,31 @@ const auth = new Auth(); export const AuthProvider: FC = (props: any) => { const { children } = props; + const router = useRouter(); const [isLoading, setIsLoading] = useState( !isSsr() /* ssr doesn't need loader */ ); + const handleVerificationRedirect = useCallback(() => { + const {is, success, message} = justVerifiedEmail(router); + if (is) { + const text = `Verification result: ${message}`; + if (success) { + toast.success(text); + } else { + toast.error(text); + } + router.push('/'); + } + return is; + }, [router]) + useEffect(() => { + const isJustVerified = handleVerificationRedirect(); + if (isJustVerified) { + auth.forgetUser(); + return; + } auth .renewSession() .catch((e) => { @@ -20,14 +41,14 @@ export const AuthProvider: FC = (props: any) => { <>
Something went wrong, please login again
If the problem persists, please contact us.
-
Error: {e}
+
Error: {typeof e === 'string' ? e : JSON.stringify(e)}
); }) .finally(() => { setIsLoading(false); }); - }, []); + }, [handleVerificationRedirect]); if (isLoading) { return <>; @@ -38,5 +59,17 @@ export const AuthProvider: FC = (props: any) => { export function useAuth(): Auth { const auth = useContext(AuthContext); + if (!auth) { + throw new Error(`"useAuth" has to be called inside AuthProvider`); + } return auth; } + +function justVerifiedEmail(router: NextRouter) { + const { query: {email, success, message} } = router; + return { + is: !!email && !!message && !!success, + success: success === 'true', + message + } +} \ No newline at end of file diff --git a/src/context/userContext/UserContext.tsx b/src/context/userContext/UserContext.tsx index 68b314db..941ad68d 100644 --- a/src/context/userContext/UserContext.tsx +++ b/src/context/userContext/UserContext.tsx @@ -2,12 +2,14 @@ import React, { FC, useContext, useEffect, useState } from 'react'; import { User } from '../../types/models'; import { useAuth } from '../authContext/AuthContext'; import { useApi } from '../apiContext/ApiContext'; +import { daysAgo } from '../../helpers/time'; type EmailNotVerifiedInfo = { isVerified: true; } | { - isVerified: false; email: string; + isVerified: false; + isRegisteredRecently: boolean; } type UserProviderContext = { @@ -16,6 +18,7 @@ type UserProviderContext = { isLoading: boolean; currentUser?: User; emailVerifedInfo?: EmailNotVerifiedInfo; + isNotYetVerified: boolean; isAuthenticated: boolean; updateCurrentUser(user: User): void; logout(): void; @@ -34,6 +37,7 @@ export const UserProvider: FC = ({ children }) => { const isAuthenticated = auth.isAuthenticated(); const isMentor = !!currentUser?.roles?.includes('Mentor'); const isAdmin = !!currentUser?.roles?.includes('Admin'); + const isNotYetVerified = emailVerifedInfo?.isVerified === false; const logout = () => { auth.doLogout(api); @@ -49,6 +53,7 @@ export const UserProvider: FC = ({ children }) => { setEmailVerifedInfo({ isVerified: user.email_verified, + isRegisteredRecently: daysAgo(user.createdAt) <= 5, email: user.email, }); @@ -67,6 +72,7 @@ export const UserProvider: FC = ({ children }) => { isLoading, currentUser, emailVerifedInfo, + isNotYetVerified, isAuthenticated, logout, updateCurrentUser, diff --git a/src/utils/auth.js b/src/utils/auth.js index 8951a54f..e651cb0a 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -1,4 +1,5 @@ import auth0 from 'auth0-js'; +import ApiService from '../api'; import { isSsr } from '../helpers/ssr'; import { isMentor } from '../helpers/user'; @@ -141,9 +142,9 @@ class Auth { } catch (error) { reject(error); } - } else if (!this.isAuthenticated()) { + } else { this.auth0.checkSession({}, (err, authResult) => { - if (err) { + if (err && !this.#shouldSkipError(err)) { return reject(err); } if (authResult && authResult.accessToken && authResult.idToken) { @@ -151,13 +152,14 @@ class Auth { } resolve(); }); - } else { - resolve(); } }); } - #logout = () => { + /** + * @param {ApiService=} api + */ + forgetUser = (api) => { // Remove tokens and expiry time from memory this.accessToken = null; this.idToken = null; @@ -165,12 +167,16 @@ class Auth { // Remove token from localStorage localStorage.removeItem(storageKey); + ApiService.clearCurrentUserFromStorage(); + api?.clearCurrentUser(); }; // TODO: figure out why the API service needs to clear the current user instead of the Auth class? + /** + * @param {ApiService} api + */ doLogout = (api) => { - this.#logout(); - api.clearCurrentUser(); + this.forgetUser(api); this.auth0.logout({ returnTo: this.redirectUri, }); @@ -182,6 +188,10 @@ class Auth { let expiresAt = this.expiresAt; return new Date().getTime() < expiresAt; } + + #shouldSkipError = (error) => { + return error.code === 'login_required'; + } } export default Auth; From 45bff76e52632f4e81468fa00125b8f500217c89 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Wed, 4 May 2022 12:34:25 +0300 Subject: [PATCH 6/8] prettier --- src/utils/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/auth.js b/src/utils/auth.js index e651cb0a..e1d5a575 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -191,7 +191,7 @@ class Auth { #shouldSkipError = (error) => { return error.code === 'login_required'; - } + }; } export default Auth; From 34d942ebcfb5ce80fa69ce4b2af955619d922c35 Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Fri, 6 May 2022 08:57:02 +0300 Subject: [PATCH 7/8] typo --- src/components/layouts/App/App.js | 8 ++++---- src/components/layouts/App/VerificationModal.tsx | 8 ++++---- src/context/userContext/UserContext.tsx | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/layouts/App/App.js b/src/components/layouts/App/App.js index f7be2fcd..32c62dbd 100644 --- a/src/components/layouts/App/App.js +++ b/src/components/layouts/App/App.js @@ -24,7 +24,7 @@ const App = (props) => { onClose: null, }); const { mentors } = useMentors(); - const { emailVerifedInfo } = useUser(); + const { emailVerifiedInfo } = useUser(); const closeModal = useCallback(() => setModal({}), []); const showVerifyEmailModal = useCallback(() => { @@ -58,10 +58,10 @@ const App = (props) => { }, []); useEffect(() => { - if (emailVerifedInfo?.isVerified === false) { - showVerifyEmailModal(emailVerifedInfo.email); + if (emailVerifiedInfo?.isVerified === false) { + showVerifyEmailModal(emailVerifiedInfo.email); } - }, [emailVerifedInfo, showVerifyEmailModal]); + }, [emailVerifiedInfo, showVerifyEmailModal]); useEffect( () => setWindowTitle({ tag, country, name, language }), diff --git a/src/components/layouts/App/VerificationModal.tsx b/src/components/layouts/App/VerificationModal.tsx index 7321f4c7..7a480202 100644 --- a/src/components/layouts/App/VerificationModal.tsx +++ b/src/components/layouts/App/VerificationModal.tsx @@ -18,10 +18,10 @@ const ModalText = styled.p` export const VerificationModal = ({ onSuccess }: VerificationModalProps) => { const [loading, setLoading] = useState(false); - const { emailVerifedInfo } = useUser(); + const { emailVerifiedInfo } = useUser(); const api = useApi(); - if (emailVerifedInfo.isVerified === true) { + if (emailVerifiedInfo.isVerified === true) { // eslint-disable-next-line no-console console.warn('email is verified'); return; @@ -44,8 +44,8 @@ export const VerificationModal = ({ onSuccess }: VerificationModalProps) => { Psst, we believe that you are who you say you are.
Just to make sure, we need you to verify your email. -

Recognize {maskEmail(emailVerifedInfo.email)}?

- {emailVerifedInfo.isRegisteredRecently ? ( +

Recognize {maskEmail(emailVerifiedInfo.email)}?

+ {emailVerifiedInfo.isRegisteredRecently ? ( <> This is the address we sent a verification email to.
diff --git a/src/context/userContext/UserContext.tsx b/src/context/userContext/UserContext.tsx index 941ad68d..0dfd23d4 100644 --- a/src/context/userContext/UserContext.tsx +++ b/src/context/userContext/UserContext.tsx @@ -17,7 +17,7 @@ type UserProviderContext = { isMentor: boolean; isLoading: boolean; currentUser?: User; - emailVerifedInfo?: EmailNotVerifiedInfo; + emailVerifiedInfo?: EmailNotVerifiedInfo; isNotYetVerified: boolean; isAuthenticated: boolean; updateCurrentUser(user: User): void; @@ -31,13 +31,13 @@ const UserContext = React.createContext( export const UserProvider: FC = ({ children }) => { const [isLoading, setIsloading] = useState(true); const [currentUser, updateCurrentUser] = useState(); - const [emailVerifedInfo, setEmailVerifedInfo] = useState(); + const [emailVerifiedInfo, setemailVerifiedInfo] = useState(); const auth = useAuth(); const api = useApi(); const isAuthenticated = auth.isAuthenticated(); const isMentor = !!currentUser?.roles?.includes('Mentor'); const isAdmin = !!currentUser?.roles?.includes('Admin'); - const isNotYetVerified = emailVerifedInfo?.isVerified === false; + const isNotYetVerified = emailVerifiedInfo?.isVerified === false; const logout = () => { auth.doLogout(api); @@ -51,7 +51,7 @@ export const UserProvider: FC = ({ children }) => { return; } - setEmailVerifedInfo({ + setemailVerifiedInfo({ isVerified: user.email_verified, isRegisteredRecently: daysAgo(user.createdAt) <= 5, email: user.email, @@ -71,7 +71,7 @@ export const UserProvider: FC = ({ children }) => { isMentor, isLoading, currentUser, - emailVerifedInfo, + emailVerifiedInfo, isNotYetVerified, isAuthenticated, logout, From 5e14c29c1d6ca4dd701dff2be80c4ed4f5d4a0ef Mon Sep 17 00:00:00 2001 From: Moshe Feuchtwanger Date: Fri, 6 May 2022 09:10:54 +0300 Subject: [PATCH 8/8] make a bit more readable --- src/utils/auth.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utils/auth.js b/src/utils/auth.js index e1d5a575..752a8ac3 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -157,7 +157,7 @@ class Auth { } /** - * @param {ApiService=} api + * @param {ApiService=} api - it's empty when called from AuthContext because it's hieghr in the Providers tree */ forgetUser = (api) => { // Remove tokens and expiry time from memory @@ -167,8 +167,11 @@ class Auth { // Remove token from localStorage localStorage.removeItem(storageKey); - ApiService.clearCurrentUserFromStorage(); - api?.clearCurrentUser(); + if (api) { + api?.clearCurrentUser(); + } else { + ApiService.clearCurrentUserFromStorage(); + } }; // TODO: figure out why the API service needs to clear the current user instead of the Auth class?