Skip to content

Commit

Permalink
feat: complete registration screen (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
cka-y authored Nov 6, 2023
1 parent ffa1896 commit ea9a341
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 63 deletions.
File renamed without changes.
7 changes: 7 additions & 0 deletions web-app/src/app/router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ import SignUp from '../screens/SingUp';
import Account from '../screens/Account';
import ContactInformation from '../screens/ContactInformation';
import { ProtectedRoute } from './ProtectedRoute';
import CompleteRegistration from '../screens/CompleteRegistration';

export const AppRouter: React.FC = () => {
return (
<Routes>
<Route path='/' element={<SignIn />} />
<Route path='sign-in' element={<SignIn />} />
<Route path='sign-up' element={<SignUp />} />
<Route element={<ProtectedRoute />}>
<Route
path='complete-registration'
element={<CompleteRegistration />}
/>
</Route>
<Route element={<ProtectedRoute />}>
<Route path='account' element={<Account />} />
</Route>
Expand Down
185 changes: 185 additions & 0 deletions web-app/src/app/screens/CompleteRegistration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { getAuth } from 'firebase/auth';
import 'firebase/firestore';
import {
Alert,
Box,
Checkbox,
CssBaseline,
FormControlLabel,
} from '@mui/material';
import { useAppDispatch } from '../hooks';
import { refreshUserInformation } from '../store/profile-reducer';
import { useNavigate } from 'react-router-dom';
import { ACCOUNT_TARGET } from '../constants/Navigation';
import {
selectIsRegistered,
selectRegistrationError,
} from '../store/profile-selectors';
import { useSelector } from 'react-redux';

export default function CompleteRegistration(): React.ReactElement {
const auth = getAuth();
const user = auth.currentUser;
const dispatch = useAppDispatch();
const navigateTo = useNavigate();

const isRegistered = useSelector(selectIsRegistered);
const registrationError = useSelector(selectRegistrationError);

React.useEffect(() => {
if (isRegistered) {
navigateTo(ACCOUNT_TARGET);
}
}, [isRegistered]);

const CompleteRegistrationSchema = Yup.object().shape({
fullname: Yup.string().required('Your full name is required.'),
requiredCheck: Yup.boolean().oneOf([true], 'This field must be checked'),
agreeToTerms: Yup.boolean()
.required('You must accept the terms and conditions.')
.isTrue('You must accept the terms and conditions.'),
agreeToPrivacyPolicy: Yup.boolean()
.required('You must agree to the privacy policy.')
.isTrue('You must agree to the privacy policy.'),
});

const formik = useFormik({
initialValues: {
fullname: '',
organizationName: '',
receiveAPIannouncements: false,
agreeToTerms: false,
agreeToPrivacyPolicy: false,
},
validationSchema: CompleteRegistrationSchema,
onSubmit: async (values) => {
if (user != null) {
dispatch(
refreshUserInformation({
fullname: values?.fullname,
organization: values?.organizationName,
}),
);
}
},
});

return (
<Container component='main' maxWidth='sm'>
<CssBaseline />
<Box
sx={{
mt: 12,
display: 'flex',
flexDirection: 'column',
}}
>
<Typography
component='h1'
variant='h5'
color='primary'
fontWeight='bold'
>
Your API Account
</Typography>
<Typography component='h1' variant='h6' color='primary'>
Contact Information
</Typography>
<form onSubmit={formik.handleSubmit} noValidate>
<TextField
margin='normal'
required
fullWidth
id='fullname'
label='Full Name'
name='fullname'
autoFocus
onChange={formik.handleChange}
value={formik.values.fullname}
error={formik.errors.fullname != null}
/>
{formik.errors.fullname != null ? (
<Alert severity='error'>{formik.errors.fullname}</Alert>
) : null}
<TextField
margin='normal'
fullWidth
id='organizationName'
label='Organization Name'
name='organizationName'
onChange={formik.handleChange}
value={formik.values.organizationName}
/>
<FormControlLabel
control={
<Checkbox
id='receiveAPIannouncements'
value={formik.values.receiveAPIannouncements}
onChange={formik.handleChange}
color='primary'
/>
}
label='I would like to receive new API release announcements via email.'
sx={{ width: '100%' }}
/>
<FormControlLabel
control={
<Checkbox
id='agreeToTerms'
required
value={formik.values.agreeToTerms}
onChange={formik.handleChange}
color='primary'
/>
}
label='I agree to the terms and conditions'
sx={{ width: '100%' }}
/>
{formik.errors.agreeToTerms != null ? (
<Alert severity='error'>{formik.errors.agreeToTerms}</Alert>
) : null}
<FormControlLabel
control={
<Checkbox
id='agreeToPrivacyPolicy'
required
value={formik.values.agreeToPrivacyPolicy}
onChange={formik.handleChange}
color='primary'
/>
}
label='I have read and agree to the privacy policy.'
sx={{ width: '100%' }}
/>
{formik.errors.agreeToPrivacyPolicy != null ? (
<Alert severity='error'>{formik.errors.agreeToPrivacyPolicy}</Alert>
) : null}

{/* TODO: Add Captcha Here */}
<Box
width={'100%'}
sx={{
mt: 2,
display: 'flex',
justifyContent: 'center',
}}
>
<Button type='submit' variant='contained'>
Finish Account Setup
</Button>
{registrationError != null ? (
<Alert severity='error'>{registrationError.message}</Alert>
) : null}
</Box>
</form>
</Box>
</Container>
);
}
22 changes: 13 additions & 9 deletions web-app/src/app/services/profile-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type AdditionalUserInfo } from 'firebase/auth';
import { updateProfile, type AdditionalUserInfo } from 'firebase/auth';
import { app } from '../../firebase';
import { type OauthProvider, type User } from '../types';

Expand Down Expand Up @@ -57,6 +57,18 @@ export const generateUserAccessToken = async (): Promise<User | null> => {
};
};

export const updateUserInformation = async (values: {
fullname: string;
}): Promise<void> => {
const currentUser = app.auth().currentUser;
// TODO: this is to be removed and replaced by storing the information in Datastore
if (currentUser !== null) {
await updateProfile(currentUser, {
displayName: values?.fullname,
});
}
};

export const populateUserWithAdditionalInfo = (
user: User,
additionalUserInfo: AdditionalUserInfo,
Expand All @@ -72,11 +84,3 @@ export const populateUserWithAdditionalInfo = (
user?.email ?? (additionalUserInfo.profile?.email as string) ?? undefined,
};
};

export const getUserOrganization = async (): Promise<string> => {
throw new Error('Not implemented');
};

export const saveOrganization = async (): Promise<string> => {
throw new Error('Not implemented');
};
35 changes: 25 additions & 10 deletions web-app/src/app/store/profile-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ interface UserProfileState {
| 'authenticated'
| 'login_out'
| 'sign_up'
| 'loading_organization';
| 'registered'
| 'registering';
errors: AppErrors;
user: User | undefined;
}
Expand All @@ -28,6 +29,7 @@ const initialState: UserProfileState = {
SignUp: null,
Login: null,
Logout: null,
Registration: null,
},
};

Expand Down Expand Up @@ -85,15 +87,6 @@ export const userProfileSlice = createSlice({
state.status = 'unauthenticated';
state.errors = { ...initialState.errors, SignUp: action.payload };
},
loadOrganization: (state) => {
state.status = 'loading_organization';
},
loadOrganizationSuccess: (state, action: PayloadAction<string>) => {
state.status = 'authenticated';
if (state.user !== undefined) {
state.user.organization = action.payload;
}
},
resetProfileErrors: (state) => {
state.errors = { ...initialState.errors };
},
Expand All @@ -115,6 +108,25 @@ export const userProfileSlice = createSlice({
state.status = 'authenticated';
state.errors = { ...initialState.errors };
},
refreshUserInformation: (
state,
action: PayloadAction<{ fullname: string; organization: string }>,
) => {
if (state.user !== undefined && state.status === 'authenticated') {
state.errors.Registration = null;
state.user.fullname = action.payload?.fullname ?? '';
state.user.organization = action.payload?.organization ?? 'Unknown';
state.status = 'registering';
}
},
refreshUserInformationFail: (state, action: PayloadAction<AppError>) => {
state.errors.Registration = action.payload;
state.status = 'authenticated';
},
refreshUserInformationSuccess: (state) => {
state.errors.Registration = null;
state.status = 'registered';
},
},
});

Expand All @@ -132,6 +144,9 @@ export const {
refreshAccessToken,
requestRefreshAccessToken,
loginWithProvider,
refreshUserInformation,
refreshUserInformationFail,
refreshUserInformationSuccess,
} = userProfileSlice.actions;

export default userProfileSlice.reducer;
8 changes: 7 additions & 1 deletion web-app/src/app/store/profile-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ export const selectUserProfile = (state: RootState): User | undefined =>

export const selectIsAuthenticated = (state: RootState): boolean =>
state.userProfile.status === 'authenticated' ||
state.userProfile.status === 'loading_organization';
state.userProfile.status === 'registered';

export const selectIsRegistered = (state: RootState): boolean =>
state.userProfile.status === 'registered';

export const selectErrorBySource = (
state: RootState,
Expand All @@ -18,3 +21,6 @@ export const selectEmailLoginError = (state: RootState): AppError | null =>

export const selectSignUpError = (state: RootState): AppError | null =>
selectErrorBySource(state, ErrorSource.SignUp);

export const selectRegistrationError = (state: RootState): AppError | null =>
selectErrorBySource(state, ErrorSource.Registration);
21 changes: 1 addition & 20 deletions web-app/src/app/store/saga/auth-saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { app } from '../../../firebase';
import { type PayloadAction } from '@reduxjs/toolkit';
import { call, put, takeLatest } from 'redux-saga/effects';
import {
type AppError,
USER_PROFILE_LOGIN,
USER_PROFILE_LOGOUT,
USER_PROFILE_SIGNUP,
Expand All @@ -19,7 +18,6 @@ import {
signUpFail,
signUpSuccess,
} from '../profile-reducer';
import { FirebaseError } from '@firebase/util';
import { type NavigateFunction } from 'react-router-dom';
import {
getUserFromSession,
Expand All @@ -31,24 +29,7 @@ import {
type UserCredential,
getAdditionalUserInfo,
} from 'firebase/auth';

const getAppError = (error: unknown): AppError => {
const appError: AppError = {
code: 'unknown',
message: 'Unknown error',
};
if (error instanceof FirebaseError) {
appError.code = error.code;
let message = error.message;
if (error.message.startsWith('Firebase: ')) {
message = error.message.substring('Firebase: '.length);
}
appError.message = message;
} else {
appError.message = error as string;
}
return appError;
};
import { getAppError } from '../../utils/error';

function* emailLoginSaga({
payload: { email, password },
Expand Down
Loading

0 comments on commit ea9a341

Please sign in to comment.