From c0ab0f96a0a7d595ee5bf13e78588f9046da5a99 Mon Sep 17 00:00:00 2001 From: Luca Micieli Date: Sun, 3 Jan 2021 14:13:49 +0100 Subject: [PATCH] feat(app-signup): add email verification functionality --- package.json | 1 + packages/game-app/src/App.tsx | 8 +++-- packages/game-app/src/_shared/auth/hooks.ts | 4 +++ packages/game-app/src/_shared/auth/index.ts | 4 +-- packages/game-app/src/_shared/auth/saga.ts | 5 +++ packages/game-app/src/_shared/auth/slice.ts | 1 + .../_shared/requests-status/requestsKeys.ts | 1 + .../game-app/src/_shared/routing/index.ts | 3 +- .../src/_shared/routing/useQueryParams.ts | 9 +++++ packages/game-app/src/assets/i18n/en.ts | 5 +++ .../components/VerifyEmail/VerifyEmail.tsx | 34 +++++++++++++++++++ .../signup/components/VerifyEmail/index.ts | 3 ++ .../src/signup/types/emailValidationParams.ts | 6 ++++ 13 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 packages/game-app/src/_shared/routing/useQueryParams.ts create mode 100644 packages/game-app/src/signup/components/VerifyEmail/VerifyEmail.tsx create mode 100644 packages/game-app/src/signup/components/VerifyEmail/index.ts create mode 100644 packages/game-app/src/signup/types/emailValidationParams.ts diff --git a/package.json b/package.json index 423a30fc..7058eb86 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "bootstrap": "npm ci && npx lerna bootstrap", "build": "npx lerna run build --stream", + "start:emulators": "npx firebase emulators:start", "test:firestore": "npm run test --prefix packages/firestore", "scripts:initialize-firestore-emulator": "node scripts/load-initial-data-in-emulator.js", "scripts:initialize-firestore-emulator:local": "npx env-cmd -f .env node scripts/load-initial-data-in-emulator.js" diff --git a/packages/game-app/src/App.tsx b/packages/game-app/src/App.tsx index 286f3736..771ad3a3 100644 --- a/packages/game-app/src/App.tsx +++ b/packages/game-app/src/App.tsx @@ -6,6 +6,7 @@ import { useLoggedUser } from '@pipeline/auth'; const Signup = React.lazy(() => import('./signup/components/Signup')); const EmailVerificationRequired = React.lazy(() => import('./signup/components/EmailVerificationRequired')); +const VerifyEmail = React.lazy(() => import('./signup/components/VerifyEmail')); function App() { const bootstrapIsFinished = useBootstrapIsFinished(); @@ -17,7 +18,10 @@ function App() { return bootstrapIsFinished ? ( - {user && !user.emailVerified && pathname !== RoutingPath.EmailVerificationRequired ? ( + {user && + !user.emailVerified && + pathname !== RoutingPath.EmailVerificationRequired && + pathname !== RoutingPath.VerifyEmail ? ( @@ -25,7 +29,7 @@ function App() {
Login
} /> -
VerifyEmail
} /> +
Dashboard
} /> diff --git a/packages/game-app/src/_shared/auth/hooks.ts b/packages/game-app/src/_shared/auth/hooks.ts index 8e129ec3..d68345b5 100644 --- a/packages/game-app/src/_shared/auth/hooks.ts +++ b/packages/game-app/src/_shared/auth/hooks.ts @@ -5,3 +5,7 @@ export const useResendVerificationEmail = createRequestHook( 'auth.resendVerificationEmail', actions.resendEmailVerification, ); + +export const useEmailVerification = createRequestHook('auth.emailVerification', actions.verifyEmail, { + errorMessagesScope: 'auth.errors', +}); diff --git a/packages/game-app/src/_shared/auth/index.ts b/packages/game-app/src/_shared/auth/index.ts index 4f35f68f..146d26a5 100644 --- a/packages/game-app/src/_shared/auth/index.ts +++ b/packages/game-app/src/_shared/auth/index.ts @@ -1,8 +1,8 @@ import { reducer, actions, name, selectors, AuthUser } from './slice'; import saga from './saga'; import useLoggedUser from './useLoggedUser'; -import { useResendVerificationEmail } from './hooks'; +import { useEmailVerification, useResendVerificationEmail } from './hooks'; -export { reducer, actions, name, saga, selectors, useLoggedUser, useResendVerificationEmail }; +export { reducer, actions, name, saga, selectors, useLoggedUser, useResendVerificationEmail, useEmailVerification }; export type { AuthUser }; diff --git a/packages/game-app/src/_shared/auth/saga.ts b/packages/game-app/src/_shared/auth/saga.ts index f4a0fca0..e5a9f876 100644 --- a/packages/game-app/src/_shared/auth/saga.ts +++ b/packages/game-app/src/_shared/auth/saga.ts @@ -29,10 +29,15 @@ function* resendVerificationEmail() { yield call(() => firebase.auth().currentUser?.sendEmailVerification()); } +function* executeEmailVerification(action: ReturnType) { + yield call(() => firebase.auth().applyActionCode(action.payload.code)); +} + export default function* authSaga() { yield takeEvery(actions.initialize, initializeAuthSaga); yield takeEvery( actions.resendEmailVerification, addRequestStatusManagement(resendVerificationEmail, 'auth.resendVerificationEmail'), ); + yield takeEvery(actions.verifyEmail, addRequestStatusManagement(executeEmailVerification, 'auth.emailVerification')); } diff --git a/packages/game-app/src/_shared/auth/slice.ts b/packages/game-app/src/_shared/auth/slice.ts index 7d223818..67252f5f 100644 --- a/packages/game-app/src/_shared/auth/slice.ts +++ b/packages/game-app/src/_shared/auth/slice.ts @@ -47,6 +47,7 @@ export const name = slice.name; export const actions = { ...slice.actions, resendEmailVerification: createAction(`${name}/resendEmailVerification`), + verifyEmail: createAction<{ code: string }>(`${name}/verifyEmail`), }; export const selectors = { getCurrentUser, diff --git a/packages/game-app/src/_shared/requests-status/requestsKeys.ts b/packages/game-app/src/_shared/requests-status/requestsKeys.ts index 43fba172..9d6aa6cf 100644 --- a/packages/game-app/src/_shared/requests-status/requestsKeys.ts +++ b/packages/game-app/src/_shared/requests-status/requestsKeys.ts @@ -4,4 +4,5 @@ export interface RequestsKeys { gameRoles: null; devOpsMaturities: null; 'auth.resendVerificationEmail': null; + 'auth.emailVerification': null; } diff --git a/packages/game-app/src/_shared/routing/index.ts b/packages/game-app/src/_shared/routing/index.ts index 68788f94..f16f0380 100644 --- a/packages/game-app/src/_shared/routing/index.ts +++ b/packages/game-app/src/_shared/routing/index.ts @@ -1,5 +1,6 @@ import { RoutingPath } from './routingPath'; import PrivateRoute from './PrivateRoute'; import useNavigateOnCondition from './useNavigateOnCondition'; +import { useQueryParams } from './useQueryParams'; -export { RoutingPath, PrivateRoute, useNavigateOnCondition }; +export { RoutingPath, PrivateRoute, useNavigateOnCondition, useQueryParams }; diff --git a/packages/game-app/src/_shared/routing/useQueryParams.ts b/packages/game-app/src/_shared/routing/useQueryParams.ts new file mode 100644 index 00000000..ae478042 --- /dev/null +++ b/packages/game-app/src/_shared/routing/useQueryParams.ts @@ -0,0 +1,9 @@ +import { useLocation } from 'react-router-dom'; + +/** + * Hook to parse query params and get them as object + */ +export function useQueryParams() { + const { search } = useLocation(); + return (Object.fromEntries(new URLSearchParams(search).entries()) as unknown) as T; +} diff --git a/packages/game-app/src/assets/i18n/en.ts b/packages/game-app/src/assets/i18n/en.ts index bc5ce76e..533c2509 100644 --- a/packages/game-app/src/assets/i18n/en.ts +++ b/packages/game-app/src/assets/i18n/en.ts @@ -7,6 +7,11 @@ const translations = { test1: 'Pipeline - The Game that Delivers!', test2: 'Random stuff', }, + auth: { + errors: { + 'auth/invalid-action-code': 'Verification link invalid or already used', + }, + }, signup: { verificationRequired: { message: "You need to verify your email to start playing! If you don't find it, try in your spam", diff --git a/packages/game-app/src/signup/components/VerifyEmail/VerifyEmail.tsx b/packages/game-app/src/signup/components/VerifyEmail/VerifyEmail.tsx new file mode 100644 index 00000000..11ae0e02 --- /dev/null +++ b/packages/game-app/src/signup/components/VerifyEmail/VerifyEmail.tsx @@ -0,0 +1,34 @@ +import React, { useEffect } from 'react'; +import { VerifyEmailParams } from '../../types/emailValidationParams'; +import { RoutingPath, useNavigateOnCondition, useQueryParams } from '@pipeline/routing'; +import { useEmailVerification, useLoggedUser } from '@pipeline/auth'; + +type Props = {}; + +const VerifyEmail: React.FC = () => { + let params = useQueryParams(); + + const { call, success, translatedError } = useEmailVerification(); + const loggedUser = useLoggedUser(); + + useEffect(() => { + call({ code: params.oobCode }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useNavigateOnCondition(success, loggedUser ? RoutingPath.Dashboard : RoutingPath.Login); + + return ( +
+ {translatedError ? ( +
+ {translatedError} +
+ ) : null} +
+ ); +}; + +VerifyEmail.displayName = 'VerifyEmail'; + +export default VerifyEmail; diff --git a/packages/game-app/src/signup/components/VerifyEmail/index.ts b/packages/game-app/src/signup/components/VerifyEmail/index.ts new file mode 100644 index 00000000..cf345346 --- /dev/null +++ b/packages/game-app/src/signup/components/VerifyEmail/index.ts @@ -0,0 +1,3 @@ +import VerifyEmail from './VerifyEmail'; + +export default VerifyEmail; diff --git a/packages/game-app/src/signup/types/emailValidationParams.ts b/packages/game-app/src/signup/types/emailValidationParams.ts new file mode 100644 index 00000000..88ff126a --- /dev/null +++ b/packages/game-app/src/signup/types/emailValidationParams.ts @@ -0,0 +1,6 @@ +export interface VerifyEmailParams { + mode: string; + lang: string; + oobCode: string; + apiKey: string; +}