Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: complete registration screen #151

Merged
merged 5 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading