Skip to content

Commit e5f96cf

Browse files
Phase 4 - Implement Email Verification Flow for User Registration
- Add VerificationPage and VerificationForm components - Integrate email verification with Cognito resend confirmation code - Update routing to support verification page - Enhance auth translations with verification-related messages - Implement form validation and error handling for verification process
1 parent 22ea3fd commit e5f96cf

File tree

9 files changed

+315
-9
lines changed

9 files changed

+315
-9
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import TabNavigation from './TabNavigation';
77
import SignInPage from 'pages/Auth/SignIn/SignInPage';
88
import SignUpPage from 'pages/Auth/SignUp/SignUpPage';
99
import SignOutPage from 'pages/Auth/SignOut/SignOutPage';
10+
import VerificationPage from 'pages/Auth/Verify/VerificationPage';
1011

1112
/**
1213
* The application router. This is the main router for the Ionic React
@@ -31,6 +32,7 @@ const AppRouter = (): JSX.Element => {
3132
/>
3233
<Route exact path="/auth/signin" render={() => <SignInPage />} />
3334
<Route exact path="/auth/signup" render={() => <SignUpPage />} />
35+
<Route exact path="/auth/verify" render={() => <VerificationPage />} />
3436
<Route exact path="/auth/signout" render={() => <SignOutPage />} />
3537
<Route exact path="/">
3638
<Redirect to="/tabs" />

frontend/src/common/hooks/useAuthOperations.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,10 @@ export const useAuthOperations = () => {
102102
const resendConfirmationCode = async (email: string): Promise<void> => {
103103
setIsLoading(true);
104104
clearError();
105-
console.log('resendConfirmationCode', email);
106105

107106
try {
108-
// This would need to be implemented in the Cognito service
109-
// For now, we'll throw an error
110-
throw new Error('Not implemented');
107+
await CognitoAuthService.resendConfirmationCode(email);
108+
// Success - code resent to user's email
111109
} catch (err) {
112110
setError(formatAuthError(err));
113111
throw err;

frontend/src/common/services/auth/cognito-auth-service.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { signIn, signUp, confirmSignUp, signOut,
2-
fetchAuthSession, getCurrentUser } from '@aws-amplify/auth';
2+
fetchAuthSession, getCurrentUser, resendSignUpCode } from '@aws-amplify/auth';
33
import { Amplify } from 'aws-amplify';
44
import { amplifyConfig } from '../../config/aws-config';
55
import { UserTokens } from '../../models/auth';
@@ -74,6 +74,20 @@ export class CognitoAuthService {
7474
}
7575
}
7676

77+
/**
78+
* Resend confirmation code to the user's email
79+
* @param username Email address
80+
* @returns Promise resolving when the code is sent
81+
*/
82+
static async resendConfirmationCode(username: string) {
83+
try {
84+
return await resendSignUpCode({ username });
85+
} catch (error) {
86+
this.handleAuthError(error);
87+
throw error;
88+
}
89+
}
90+
7791
/**
7892
* Sign out the current user
7993
* @returns Promise resolving when sign out is complete

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"error": {
3-
"unable-to-verify": "We were unable to verify your credentials. Please try again."
3+
"unable-to-verify": "We were unable to verify your credentials. Please try again.",
4+
"no-email": "Email address is required for verification."
45
},
56
"info-username": {
67
"part1": "This example application uses ",
@@ -35,6 +36,12 @@
3536
"email-verification": {
3637
"title": "Email Verification",
3738
"message": "We've sent a verification code to your email. Please enter it below to verify your account.",
38-
"success": "Email verified successfully!"
39+
"success": "Email verified successfully!",
40+
"code-resent": "A new verification code has been sent to your email."
41+
},
42+
"validation": {
43+
"numeric": "Must contain only numbers",
44+
"exact-length": "Must be exactly {{length}} characters",
45+
"passwords-match": "Passwords must match"
3946
}
4047
}

frontend/src/pages/Auth/SignUp/components/SignUpForm.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,11 @@ const SignUpForm = ({ className, testid = 'form-signup' }: SignUpFormProps): JSX
102102
setShowProgress(true);
103103
await signUp(values.email, values.password, values.firstName, values.lastName);
104104

105-
// If no error, redirect to sign in page
106-
router.push('/auth/signin', 'forward', 'replace');
105+
// Store the email in sessionStorage for the verification page
106+
sessionStorage.setItem('verification_email', values.email);
107+
108+
// Navigate to verification page
109+
router.push('/auth/verify', 'forward', 'replace');
107110
} catch (err) {
108111
setError(getAuthErrorMessage(err));
109112
} finally {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.ls-verification-page {
2+
.ls-verification-page__container {
3+
max-width: 32rem;
4+
height: 100%;
5+
display: flex;
6+
flex-direction: column;
7+
justify-content: center;
8+
}
9+
10+
.ls-verification-page__form {
11+
width: 100%;
12+
}
13+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { IonContent, IonPage } from '@ionic/react';
2+
import { useTranslation } from 'react-i18next';
3+
import { useEffect, useState } from 'react';
4+
5+
import './VerificationPage.scss';
6+
import { PropsWithTestId } from 'common/components/types';
7+
import ProgressProvider from 'common/providers/ProgressProvider';
8+
import Header from 'common/components/Header/Header';
9+
import VerificationForm from './components/VerificationForm';
10+
import Container from 'common/components/Content/Container';
11+
12+
/**
13+
* Properties for the `VerificationPage` component.
14+
*/
15+
interface VerificationPageProps extends PropsWithTestId {}
16+
17+
/**
18+
* The `VerificationPage` renders the layout for user email verification.
19+
* @param {VerificationPageProps} props - Component properties.
20+
* @returns {JSX.Element} JSX
21+
*/
22+
const VerificationPage = ({ testid = 'page-verification' }: VerificationPageProps): JSX.Element => {
23+
const { t } = useTranslation();
24+
const [email, setEmail] = useState<string>('');
25+
26+
// Get email from sessionStorage
27+
useEffect(() => {
28+
const storedEmail = sessionStorage.getItem('verification_email');
29+
if (storedEmail) {
30+
setEmail(storedEmail);
31+
}
32+
}, []);
33+
34+
return (
35+
<IonPage className="ls-verification-page" data-testid={testid}>
36+
<ProgressProvider>
37+
<Header title={t('email-verification.title', { ns: 'auth' })} />
38+
39+
<IonContent fullscreen className="ion-padding">
40+
<Container className="ls-verification-page__container" fixed>
41+
<VerificationForm className="ls-verification-page__form" email={email} />
42+
</Container>
43+
</IonContent>
44+
</ProgressProvider>
45+
</IonPage>
46+
);
47+
};
48+
49+
export default VerificationPage;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
.ls-verification-form {
2+
padding: 1rem;
3+
4+
.ls-verification-form__message {
5+
margin: 1rem 0;
6+
display: flex;
7+
flex-direction: column;
8+
gap: 0.5rem;
9+
}
10+
11+
.ls-verification-form__email {
12+
display: block;
13+
margin-top: 0.5rem;
14+
}
15+
16+
.ls-verification-form__input {
17+
margin-bottom: 1rem;
18+
}
19+
20+
.ls-verification-form__button {
21+
margin-top: 1rem;
22+
}
23+
24+
.ls-verification-form__resend {
25+
display: flex;
26+
justify-content: center;
27+
margin-top: 1rem;
28+
}
29+
30+
.ls-verification-form__success {
31+
background-color: rgba(45, 211, 111, 0.1);
32+
border-radius: 8px;
33+
padding: 12px;
34+
margin-bottom: 1rem;
35+
text-align: center;
36+
}
37+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import {
2+
IonButton,
3+
useIonRouter,
4+
IonText,
5+
} from '@ionic/react';
6+
import { useState } from 'react';
7+
import classNames from 'classnames';
8+
import { Form, Formik } from 'formik';
9+
import { object, string } from 'yup';
10+
import { useTranslation } from 'react-i18next';
11+
12+
import './VerificationForm.scss';
13+
import { BaseComponentProps } from 'common/components/types';
14+
import { useAuth } from 'common/hooks/useAuth';
15+
import { useProgress } from 'common/hooks/useProgress';
16+
import Input from 'common/components/Input/Input';
17+
import ErrorCard from 'common/components/Card/ErrorCard';
18+
import HeaderRow from 'common/components/Text/HeaderRow';
19+
import { getAuthErrorMessage } from 'common/utils/auth-errors';
20+
21+
/**
22+
* Properties for the `VerificationForm` component.
23+
*/
24+
interface VerificationFormProps extends BaseComponentProps {
25+
email: string;
26+
}
27+
28+
/**
29+
* Email verification form values.
30+
*/
31+
interface VerificationFormValues {
32+
code: string;
33+
}
34+
35+
/**
36+
* The `VerificationForm` component renders a form for verifying a user's email with a code.
37+
* @param {VerificationFormProps} props - Component properties.
38+
* @returns {JSX.Element} JSX
39+
*/
40+
const VerificationForm = ({ className, email, testid = 'form-verification' }: VerificationFormProps): JSX.Element => {
41+
const [error, setError] = useState<string>('');
42+
const [successMessage, setSuccessMessage] = useState<string>('');
43+
const { setIsActive: setShowProgress } = useProgress();
44+
const router = useIonRouter();
45+
const { confirmSignUp, resendConfirmationCode } = useAuth();
46+
const { t } = useTranslation();
47+
48+
/**
49+
* Verification form validation schema.
50+
*/
51+
const validationSchema = object({
52+
code: string()
53+
.matches(/^\d+$/, t('validation.numeric'))
54+
.length(6, t('validation.exact-length', { length: 6 }))
55+
.required(t('validation.required')),
56+
});
57+
58+
/**
59+
* Handle resend code button click
60+
*/
61+
const handleResendCode = async () => {
62+
if (!email) {
63+
setError(t('error.no-email', { ns: 'auth' }));
64+
return;
65+
}
66+
67+
try {
68+
setError('');
69+
setSuccessMessage('');
70+
setShowProgress(true);
71+
await resendConfirmationCode(email);
72+
setSuccessMessage(t('email-verification.code-resent', { ns: 'auth' }));
73+
} catch (err) {
74+
setError(getAuthErrorMessage(err));
75+
} finally {
76+
setShowProgress(false);
77+
}
78+
};
79+
80+
return (
81+
<div className={classNames('ls-verification-form', className)} data-testid={testid}>
82+
{error && (
83+
<ErrorCard
84+
content={error}
85+
className="ion-margin-bottom"
86+
testid={`${testid}-error`}
87+
/>
88+
)}
89+
90+
{successMessage && (
91+
<div className="ls-verification-form__success" data-testid={`${testid}-success`}>
92+
<IonText color="success">{successMessage}</IonText>
93+
</div>
94+
)}
95+
96+
<Formik<VerificationFormValues>
97+
initialValues={{
98+
code: '',
99+
}}
100+
onSubmit={async (values, { setSubmitting }) => {
101+
if (!email) {
102+
setError(t('error.no-email', { ns: 'auth' }));
103+
return;
104+
}
105+
106+
try {
107+
setError('');
108+
setSuccessMessage('');
109+
setShowProgress(true);
110+
await confirmSignUp(email, values.code);
111+
112+
// Show success message briefly before redirecting
113+
setSuccessMessage(t('email-verification.success', { ns: 'auth' }));
114+
setTimeout(() => {
115+
router.push('/auth/signin', 'forward', 'replace');
116+
}, 1500);
117+
} catch (err) {
118+
setError(getAuthErrorMessage(err));
119+
} finally {
120+
setShowProgress(false);
121+
setSubmitting(false);
122+
}
123+
}}
124+
validationSchema={validationSchema}
125+
>
126+
{({ dirty, isSubmitting }) => (
127+
<Form data-testid={`${testid}-form`}>
128+
<HeaderRow border>
129+
<div>{t('email-verification.title', { ns: 'auth' })}</div>
130+
</HeaderRow>
131+
132+
<div className="ls-verification-form__message">
133+
<IonText>
134+
{t('email-verification.message', { ns: 'auth' })}
135+
</IonText>
136+
{email && (
137+
<IonText className="ls-verification-form__email">
138+
<strong>{email}</strong>
139+
</IonText>
140+
)}
141+
</div>
142+
143+
<Input
144+
name="code"
145+
label={t('label.verification-code', { ns: 'auth' })}
146+
labelPlacement="stacked"
147+
maxlength={6}
148+
className="ls-verification-form__input"
149+
data-testid={`${testid}-field-code`}
150+
type="text"
151+
inputmode="numeric"
152+
/>
153+
154+
<IonButton
155+
type="submit"
156+
color="primary"
157+
className="ls-verification-form__button"
158+
expand="block"
159+
disabled={isSubmitting || !dirty}
160+
data-testid={`${testid}-button-submit`}
161+
>
162+
{t('confirm', { ns: 'auth' })}
163+
</IonButton>
164+
165+
<div className="ls-verification-form__resend">
166+
<IonButton
167+
fill="clear"
168+
color="medium"
169+
onClick={handleResendCode}
170+
disabled={isSubmitting}
171+
data-testid={`${testid}-button-resend`}
172+
>
173+
{t('resend-code', { ns: 'auth' })}
174+
</IonButton>
175+
</div>
176+
</Form>
177+
)}
178+
</Formik>
179+
</div>
180+
);
181+
};
182+
183+
export default VerificationForm;

0 commit comments

Comments
 (0)