Skip to content

Commit

Permalink
feat(app-signup): add send verification email and verification requir…
Browse files Browse the repository at this point in the history
…ed page
  • Loading branch information
rams23 committed Jan 2, 2021
1 parent eed7463 commit 388421e
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 21 deletions.
15 changes: 14 additions & 1 deletion packages/game-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import React, { Suspense } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import { PrivateRoute, RoutingPath } from '@pipeline/routing';
import { useBootstrapIsFinished } from './_shared';
import { useLoggedUser } from '@pipeline/auth';

const Signup = React.lazy(() => import('./signup/components/Signup'));
const EmailVerificationRequired = React.lazy(() => import('./signup/components/EmailVerificationRequired'));

function App() {
const bootstrapIsFinished = useBootstrapIsFinished();

const user = useLoggedUser();

const { pathname } = useLocation();

return bootstrapIsFinished ? (
<Suspense fallback={null}>
<Switch>
{user && !user.emailVerified && pathname !== RoutingPath.EmailVerificationRequired ? (
<Route path="*">
<Redirect to={RoutingPath.EmailVerificationRequired} />
</Route>
) : null}
<Route path={RoutingPath.Login} render={() => <div>Login</div>} />
<Route path={RoutingPath.Signup} component={Signup} />
<Route path={RoutingPath.EmailVerificationRequired} component={EmailVerificationRequired} />
<Route path={RoutingPath.VerifyEmail} component={() => <div>VerifyEmail</div>} />
<PrivateRoute path={RoutingPath.Dashboard} render={() => <div>Dashboard</div>} />
<Route path="*">
<Redirect to={RoutingPath.Signup} />
Expand Down
7 changes: 7 additions & 0 deletions packages/game-app/src/_shared/auth/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createRequestHook } from '@pipeline/requests-status';
import { actions } from './slice';

export const useResendVerificationEmail = createRequestHook(
'auth.resendVerificationEmail',
actions.resendEmailVerification,
);
8 changes: 6 additions & 2 deletions packages/game-app/src/_shared/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { reducer, actions, name, selectors } from './slice';
import { reducer, actions, name, selectors, AuthUser } from './slice';
import saga from './saga';
import useLoggedUser from './useLoggedUser';
import { useResendVerificationEmail } from './hooks';

export { reducer, actions, name, saga, selectors };
export { reducer, actions, name, saga, selectors, useLoggedUser, useResendVerificationEmail };

export type { AuthUser };
18 changes: 14 additions & 4 deletions packages/game-app/src/_shared/auth/saga.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { actions, User } from './slice';
import { actions, AuthUser } from './slice';
import firebase from 'firebase/app';
import 'firebase/auth';
import { addRequestStatusManagement } from '@pipeline/requests-status';

function getCurrentUser(): Promise<User | null> {
return new Promise<User | null>(resolve => {
function getCurrentUser(): Promise<AuthUser | null> {
return new Promise<AuthUser | null>(resolve => {
firebase.auth().onAuthStateChanged(user => {
if (user) {
resolve({
id: user.uid,
email: user.email!,
emailVerified: user.emailVerified,
});
} else {
resolve(null);
Expand All @@ -19,10 +21,18 @@ function getCurrentUser(): Promise<User | null> {
}

function* initializeAuthSaga() {
const user: User | null = yield call(getCurrentUser);
const user: AuthUser | null = yield call(getCurrentUser);
yield put(actions.setLoggedUser(user));
}

function* resendVerificationEmail() {
yield call(() => firebase.auth().currentUser?.sendEmailVerification());
}

export default function* authSaga() {
yield takeEvery(actions.initialize, initializeAuthSaga);
yield takeEvery(
actions.resendEmailVerification,
addRequestStatusManagement(resendVerificationEmail, 'auth.resendVerificationEmail'),
);
}
15 changes: 10 additions & 5 deletions packages/game-app/src/_shared/auth/slice.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';

export interface User {
export interface AuthUser {
id: string;
email: string;
emailVerified: boolean;
}

export interface State {
isInitialized: boolean;
loggedUser: User | null;
loggedUser: AuthUser | null;
}

const initialState = {
Expand All @@ -19,7 +20,7 @@ const slice = createSlice({
name: 'auth',
initialState: initialState,
reducers: {
setLoggedUser(state, action: PayloadAction<User | null>) {
setLoggedUser(state, action: PayloadAction<AuthUser | null>) {
state.isInitialized = true;
state.loggedUser = action.payload;
},
Expand All @@ -41,8 +42,12 @@ const isInitialized = createSelector(
);

export const reducer = slice.reducer;
export const actions = slice.actions;
export const name = slice.name;

export const actions = {
...slice.actions,
resendEmailVerification: createAction(`${name}/resendEmailVerification`),
};
export const selectors = {
getCurrentUser,
isInitialized,
Expand Down
10 changes: 10 additions & 0 deletions packages/game-app/src/_shared/auth/useLoggedUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useSelector } from 'react-redux';
import { selectors } from './slice';

/**
* Get current logged user info. null if not logged.
*/
export default function useLoggedUser() {
const loggedUser = useSelector(selectors.getCurrentUser);
return loggedUser;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface RequestsKeys {
signup: null;
gameRoles: null;
devOpsMaturities: null;
'auth.resendVerificationEmail': null;
}
3 changes: 2 additions & 1 deletion packages/game-app/src/_shared/routing/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RoutingPath } from './routingPath';
import PrivateRoute from './PrivateRoute';
import useNavigateOnCondition from './useNavigateOnCondition';

export { RoutingPath, PrivateRoute };
export { RoutingPath, PrivateRoute, useNavigateOnCondition };
23 changes: 23 additions & 0 deletions packages/game-app/src/_shared/routing/useNavigateOnCondition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { RoutingPath } from './routingPath';

/**
*
* Conditionally navigate to the provided route once the condition becomes true
*
* @param condition the condition to check
* @param route the route to go
*/
export default function useNavigateOnCondition(condition: boolean, route: RoutingPath) {
const [alreadyNavigated, setAlreadyNavigated] = useState<boolean>(false);

const history = useHistory();

useEffect(() => {
if (condition && !alreadyNavigated) {
history.push(route);
setAlreadyNavigated(true);
}
}, [condition, history, route, alreadyNavigated]);
}
5 changes: 5 additions & 0 deletions packages/game-app/src/assets/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const translations = {
test2: 'Random stuff',
},
signup: {
verificationRequired: {
message: "You need to verify your email to start playing! If you don't find it, try in your spam",
resend: 'Resend email',
resendSuccess: 'Resend success',
},
form: {
emailLabel: 'Email',
passwordLabel: 'Password',
Expand Down
9 changes: 7 additions & 2 deletions packages/game-app/src/signup/apis/executeSignup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import firebase from 'firebase/app';
import { FirebaseCollections } from '@pipeline/common';
import 'firebase/auth';
import 'firebase/firestore';
import { User } from '../../_shared/auth/slice';
import { AuthUser } from '../../_shared/auth/slice';

export async function executeSignup(signupInfo: SignupInfo): Promise<User> {
export async function executeSignup(signupInfo: SignupInfo): Promise<AuthUser> {
const credentials = await firebase.auth().createUserWithEmailAndPassword(signupInfo.email, signupInfo.password);
if (credentials.user) {
const user = credentials.user;
Expand All @@ -15,9 +15,14 @@ export async function executeSignup(signupInfo: SignupInfo): Promise<User> {
role: signupInfo.role,
devOpsMaturity: signupInfo.devOpsMaturity,
});
const emailVerified = user.emailVerified;
if (!emailVerified) {
await user.sendEmailVerification();
}
return {
id: user.uid,
email: user.email!,
emailVerified,
};
} catch (e) {
const currentUser = firebase.auth().currentUser;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { useTranslate } from '@pipeline/i18n';
import { useResendVerificationEmail } from '@pipeline/auth';

type Props = {};

const EmailVerificationRequired: React.FC<Props> = () => {
const t = useTranslate();

const { call: resendEmail, success, loading, translatedError } = useResendVerificationEmail();

return (
<div>
{t('signup.verificationRequired.message')}

<button onClick={resendEmail}>{t('signup.verificationRequired.resend')}</button>
{success ? t('signup.verificationRequired.resendSuccess') : null}
</div>
);
};

EmailVerificationRequired.displayName = 'EmailVerificationRequired';

export default EmailVerificationRequired;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import EmailVerificationRequired from './EmailVerificationRequired';

export default EmailVerificationRequired;
3 changes: 3 additions & 0 deletions packages/game-app/src/signup/components/Signup/Signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useDevOpsMaturities, useGameRoles } from '@pipeline/dynamicData';
import { signupValidationSchema } from '../../utils/validation';
import { useTranslate } from '@pipeline/i18n';
import { PasswordInput } from '@pipeline/components';
import { RoutingPath, useNavigateOnCondition } from '@pipeline/routing';

type Props = {};

Expand Down Expand Up @@ -46,6 +47,8 @@ const Signup: React.FC<Props> = () => {
[signup, handleSubmit],
);

useNavigateOnCondition(signupSuccess, RoutingPath.EmailVerificationRequired);

return (
<div className="signup">
<div className="content">
Expand Down
10 changes: 4 additions & 6 deletions packages/game-app/src/signup/sagas/signup.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import * as actions from '../actions';
import { executeSignup } from '../apis/executeSignup';
import { call, takeLeading } from 'redux-saga/effects';
import { call, put, takeLeading } from 'redux-saga/effects';
import { addRequestStatusManagement } from '@pipeline/requests-status';
import { actions as authActions, AuthUser } from '@pipeline/auth';

function* signupSaga(action: ReturnType<typeof actions.signup>) {
try {
yield call(executeSignup, action.payload);
} catch (e) {
throw e;
}
const user: AuthUser = yield call(executeSignup, action.payload);
yield put(authActions.setLoggedUser(user));
}

export default function* runSignup() {
Expand Down

0 comments on commit 388421e

Please sign in to comment.