Skip to content

Commit 22ea3fd

Browse files
Phase 3 - Add Sign Up Page and Enhance Authentication Flow
- Implement SignUpPage and SignUpForm components - Update Icon component with Google and Apple icons - Modify SignInForm to support email login and social sign-in - Update AppRouter to include sign-up route - Enhance auth translations with sign-up related text - Refactor SignOutPage to handle sign-out errors
1 parent f0b13fa commit 22ea3fd

File tree

9 files changed

+453
-67
lines changed

9 files changed

+453
-67
lines changed

frontend/src/common/components/Icon/Icon.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IonText } from '@ionic/react';
33
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
44
import { IconProp } from '@fortawesome/fontawesome-svg-core';
55
import {
6-
faBookmark,
6+
faBookmark as faSolidBookmark,
77
faBuilding,
88
faCalendar,
99
faCircleInfo,
@@ -27,7 +27,9 @@ import {
2727
faFileLines as faSolidFileLines,
2828
faUpload,
2929
faComment,
30-
faUserCircle
30+
faUserCircle,
31+
faGlobe as faGoogle,
32+
faA as faApple
3133
} from '@fortawesome/free-solid-svg-icons';
3234
import {
3335
faFileLines as faRegularFileLines,
@@ -70,7 +72,9 @@ export type IconName =
7072
| 'userCircle'
7173
| 'users'
7274
| 'userGear'
73-
| 'xmark';
75+
| 'xmark'
76+
| 'google'
77+
| 'apple';
7478

7579
/**
7680
* Properties for the `Icon` component.
@@ -90,7 +94,7 @@ export interface IconProps
9094
*/
9195
const solidIcons: Record<IconName, IconProp> = {
9296
arrowUpFromBracket: faArrowUpFromBracket,
93-
bookmark: faBookmark,
97+
bookmark: faSolidBookmark,
9498
building: faBuilding,
9599
calendar: faCalendar,
96100
circleInfo: faCircleInfo,
@@ -114,6 +118,8 @@ const solidIcons: Record<IconName, IconProp> = {
114118
userGear: faUserGear,
115119
users: faUsers,
116120
xmark: faXmark,
121+
google: faGoogle,
122+
apple: faApple,
117123
};
118124

119125
/**

frontend/src/common/components/Router/AppRouter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Redirect, Route } from 'react-router';
55
import PrivateOutlet from './PrivateOutlet';
66
import TabNavigation from './TabNavigation';
77
import SignInPage from 'pages/Auth/SignIn/SignInPage';
8+
import SignUpPage from 'pages/Auth/SignUp/SignUpPage';
89
import SignOutPage from 'pages/Auth/SignOut/SignOutPage';
910

1011
/**
@@ -29,6 +30,7 @@ const AppRouter = (): JSX.Element => {
2930
)}
3031
/>
3132
<Route exact path="/auth/signin" render={() => <SignInPage />} />
33+
<Route exact path="/auth/signup" render={() => <SignUpPage />} />
3234
<Route exact path="/auth/signout" render={() => <SignOutPage />} />
3335
<Route exact path="/">
3436
<Redirect to="/tabs" />

frontend/src/common/utils/i18n/resources/en/auth.json

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,32 @@
99
"part4": " or ",
1010
"part5": "You may use any value as the password."
1111
},
12+
"info-email": {
13+
"part1": "Enter your email address associated with your account.",
14+
"part2": "For testing, you can create a new account by clicking on the Sign Up link below."
15+
},
1216
"label": {
1317
"password": "Password",
1418
"remember-me": "Remember me",
15-
"username": "Username"
19+
"username": "Username",
20+
"email": "Email Address",
21+
"first-name": "First Name",
22+
"last-name": "Last Name",
23+
"confirm-password": "Confirm Password",
24+
"verification-code": "Verification Code"
1625
},
17-
"signin": "Sign In"
26+
"signin": "Sign In",
27+
"signup": "Sign Up",
28+
"signout": "Sign Out",
29+
"no-account": "Don't have an account?",
30+
"already-have-account": "Already have an account?",
31+
"or-signin-with": "Or sign in with",
32+
"submit": "Submit",
33+
"confirm": "Confirm",
34+
"resend-code": "Resend Code",
35+
"email-verification": {
36+
"title": "Email Verification",
37+
"message": "We've sent a verification code to your email. Please enter it below to verify your account.",
38+
"success": "Email verified successfully!"
39+
}
1840
}

frontend/src/pages/Auth/SignIn/components/SignInForm.tsx

Lines changed: 121 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {
55
IonPopover,
66
useIonRouter,
77
useIonViewDidEnter,
8+
IonText,
9+
IonRow,
10+
IonCol,
811
} from '@ionic/react';
912
import { useRef, useState } from 'react';
1013
import classNames from 'classnames';
@@ -17,13 +20,15 @@ import { BaseComponentProps } from 'common/components/types';
1720
import { RememberMe } from 'common/models/auth';
1821
import storage from 'common/utils/storage';
1922
import { StorageKey } from 'common/utils/constants';
20-
import { useSignIn } from '../api/useSignIn';
23+
import { useSignIn } from 'common/hooks/useAuth';
2124
import { useProgress } from 'common/hooks/useProgress';
2225
import Input from 'common/components/Input/Input';
2326
import ErrorCard from 'common/components/Card/ErrorCard';
2427
import Icon from 'common/components/Icon/Icon';
2528
import HeaderRow from 'common/components/Text/HeaderRow';
2629
import CheckboxInput from 'common/components/Input/CheckboxInput';
30+
import { useSocialSignIn } from 'common/hooks/useAuth';
31+
import { getAuthErrorMessage } from 'common/utils/auth-errors';
2732

2833
/**
2934
* Properties for the `SignInForm` component.
@@ -32,11 +37,11 @@ interface SignInFormProps extends BaseComponentProps {}
3237

3338
/**
3439
* Sign in form values.
35-
* @param {string} username - A username.
40+
* @param {string} email - User's email.
3641
* @param {string} password - A password.
3742
*/
3843
interface SignInFormValues {
39-
username: string;
44+
email: string;
4045
password: string;
4146
rememberMe: boolean;
4247
}
@@ -51,14 +56,17 @@ const SignInForm = ({ className, testid = 'form-signin' }: SignInFormProps): JSX
5156
const [error, setError] = useState<string>('');
5257
const { setIsActive: setShowProgress } = useProgress();
5358
const router = useIonRouter();
54-
const { mutate: signIn } = useSignIn();
59+
const { signIn, isLoading } = useSignIn();
60+
const { signInWithGoogle, signInWithApple } = useSocialSignIn();
5561
const { t } = useTranslation();
5662

5763
/**
5864
* Sign in form validation schema.
5965
*/
6066
const validationSchema = object<SignInFormValues>({
61-
username: string().required(t('validation.required')),
67+
email: string()
68+
.email(t('validation.email'))
69+
.required(t('validation.required')),
6270
password: string().required(t('validation.required')),
6371
rememberMe: boolean().default(false),
6472
});
@@ -70,6 +78,34 @@ const SignInForm = ({ className, testid = 'form-signin' }: SignInFormProps): JSX
7078
focusInput.current?.setFocus();
7179
});
7280

81+
// Handle sign in with Google
82+
const handleGoogleSignIn = async () => {
83+
try {
84+
setError('');
85+
setShowProgress(true);
86+
await signInWithGoogle();
87+
router.push('/tabs', 'forward', 'replace');
88+
} catch (err) {
89+
setError(getAuthErrorMessage(err));
90+
} finally {
91+
setShowProgress(false);
92+
}
93+
};
94+
95+
// Handle sign in with Apple
96+
const handleAppleSignIn = async () => {
97+
try {
98+
setError('');
99+
setShowProgress(true);
100+
await signInWithApple();
101+
router.push('/tabs', 'forward', 'replace');
102+
} catch (err) {
103+
setError(getAuthErrorMessage(err));
104+
} finally {
105+
setShowProgress(false);
106+
}
107+
};
108+
73109
return (
74110
<div className={classNames('ls-signin-form', className)} data-testid={testid}>
75111
{error && (
@@ -83,32 +119,31 @@ const SignInForm = ({ className, testid = 'form-signin' }: SignInFormProps): JSX
83119
<Formik<SignInFormValues>
84120
enableReinitialize={true}
85121
initialValues={{
86-
username: rememberMe?.username ?? '',
122+
email: rememberMe?.username ?? '',
87123
password: '',
88124
rememberMe: !!rememberMe,
89125
}}
90-
onSubmit={(values, { setSubmitting }) => {
91-
setError('');
92-
setShowProgress(true);
93-
signIn(values.username, {
94-
onSuccess: () => {
95-
if (values.rememberMe) {
96-
storage.setJsonItem<RememberMe>(StorageKey.RememberMe, {
97-
username: values.username,
98-
});
99-
} else {
100-
storage.removeItem(StorageKey.RememberMe);
101-
}
102-
router.push('/tabs', 'forward', 'replace');
103-
},
104-
onError: (err: Error) => {
105-
setError(err.message);
106-
},
107-
onSettled: () => {
108-
setShowProgress(false);
109-
setSubmitting(false);
110-
},
111-
});
126+
onSubmit={async (values, { setSubmitting }) => {
127+
try {
128+
setError('');
129+
setShowProgress(true);
130+
await signIn(values.email, values.password);
131+
132+
if (values.rememberMe) {
133+
storage.setJsonItem<RememberMe>(StorageKey.RememberMe, {
134+
username: values.email,
135+
});
136+
} else {
137+
storage.removeItem(StorageKey.RememberMe);
138+
}
139+
140+
router.push('/tabs', 'forward', 'replace');
141+
} catch (err) {
142+
setError(getAuthErrorMessage(err));
143+
} finally {
144+
setShowProgress(false);
145+
setSubmitting(false);
146+
}
112147
}}
113148
validationSchema={validationSchema}
114149
>
@@ -120,22 +155,23 @@ const SignInForm = ({ className, testid = 'form-signin' }: SignInFormProps): JSX
120155
</HeaderRow>
121156

122157
<Input
123-
name="username"
124-
label={t('label.username', { ns: 'auth' })}
158+
name="email"
159+
label={t('label.email', { ns: 'auth' })}
125160
labelPlacement="stacked"
126-
maxlength={30}
127-
autocomplete="off"
161+
maxlength={50}
162+
autocomplete="email"
128163
className="ls-signin-form__input"
129164
ref={focusInput}
130-
data-testid={`${testid}-field-username`}
165+
data-testid={`${testid}-field-email`}
166+
type="email"
131167
/>
132168
<Input
133169
type="password"
134170
name="password"
135171
label={t('label.password', { ns: 'auth' })}
136172
labelPlacement="stacked"
137173
maxlength={30}
138-
autocomplete="off"
174+
autocomplete="current-password"
139175
className="ls-signin-form__input"
140176
data-testid={`${testid}-field-password`}
141177
>
@@ -155,33 +191,70 @@ const SignInForm = ({ className, testid = 'form-signin' }: SignInFormProps): JSX
155191
color="primary"
156192
className="ls-signin-form__button"
157193
expand="block"
158-
disabled={isSubmitting || !dirty}
194+
disabled={isSubmitting || !dirty || isLoading}
159195
data-testid={`${testid}-button-submit`}
160196
>
161197
{t('signin', { ns: 'auth' })}
162198
</IonButton>
163199

200+
<IonRow className="ion-text-center ion-padding">
201+
<IonCol>
202+
<IonText color="medium">
203+
{t('or-signin-with', { ns: 'auth' })}
204+
</IonText>
205+
</IonCol>
206+
</IonRow>
207+
208+
<IonRow>
209+
<IonCol>
210+
<IonButton
211+
expand="block"
212+
fill="outline"
213+
color="medium"
214+
onClick={handleGoogleSignIn}
215+
disabled={isLoading}
216+
data-testid={`${testid}-button-google`}
217+
>
218+
<Icon icon="google" slot="start" />
219+
Google
220+
</IonButton>
221+
</IonCol>
222+
<IonCol>
223+
<IonButton
224+
expand="block"
225+
fill="outline"
226+
color="dark"
227+
onClick={handleAppleSignIn}
228+
disabled={isLoading}
229+
data-testid={`${testid}-button-apple`}
230+
>
231+
<Icon icon="apple" slot="start" />
232+
Apple
233+
</IonButton>
234+
</IonCol>
235+
</IonRow>
236+
237+
<IonRow className="ion-text-center ion-padding-top">
238+
<IonCol>
239+
<IonText color="medium">
240+
{t('no-account', { ns: 'auth' })}{' '}
241+
<a href="/auth/signup">{t('signup', { ns: 'auth' })}</a>
242+
</IonText>
243+
</IonCol>
244+
</IonRow>
245+
164246
<IonPopover
165247
trigger="signinInfo"
166248
triggerAction="hover"
167249
className="ls-signin-form-popover"
168250
>
169251
<IonContent className="ion-padding">
170252
<p>
171-
{t('info-username.part1', { ns: 'auth' })}
172-
<a
173-
href="https://jsonplaceholder.typicode.com/users"
174-
target="_blank"
175-
rel="noreferrer"
176-
>
177-
{t('info-username.part2', { ns: 'auth' })}
178-
</a>
179-
. {t('info-username.part3', { ns: 'auth' })}{' '}
180-
<span className="inline-code">Bret</span>{' '}
181-
{t('info-username.part4', { ns: 'auth' })}{' '}
182-
<span className="inline-code">Samantha</span>.
253+
{t('info-email.part1', { ns: 'auth' })}
254+
</p>
255+
<p>
256+
{t('info-email.part2', { ns: 'auth' })}
183257
</p>
184-
<p>{t('info-username.part5', { ns: 'auth' })}</p>
185258
</IonContent>
186259
</IonPopover>
187260
</Form>

0 commit comments

Comments
 (0)