From 316401767cf1717a323cce447fc9e143effd4e8e Mon Sep 17 00:00:00 2001 From: lynaDev2 Date: Thu, 5 Mar 2026 01:34:18 +0000 Subject: [PATCH 1/2] feat(auth): implement email verification flow UI with token handling and resend option --- package-lock.json | 22 +++- src/features/auth/authSlice.js | 79 ++++++++++++ src/features/auth/authThunks.ts | 14 +++ src/pages/auth/VerifyEmailPage.jsx | 195 +++++++++++++++++++++++++++++ src/routes/AppRouter.jsx | 2 + 5 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 src/pages/auth/VerifyEmailPage.jsx diff --git a/package-lock.json b/package-lock.json index bac82be..d92eacd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,6 +83,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1804,6 +1805,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1851,6 +1853,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2013,6 +2016,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2179,7 +2183,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -2376,6 +2381,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3556,6 +3562,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3583,6 +3590,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3650,6 +3658,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3659,6 +3668,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3671,6 +3681,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -3713,6 +3724,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -3783,7 +3795,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-persist": { "version": "6.0.0", @@ -3950,7 +3963,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -4083,6 +4097,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4216,6 +4231,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/features/auth/authSlice.js b/src/features/auth/authSlice.js index 39b3a13..b9573bd 100644 --- a/src/features/auth/authSlice.js +++ b/src/features/auth/authSlice.js @@ -1,4 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; +import { registerUser, loginUser, logoutUser, verifyEmail, resendEmailVerification, forgotPassword } from './authThunks'; const initialState = { user: null, @@ -34,6 +35,84 @@ const authSlice = createSlice({ state.isLoading = false; }, }, + extraReducers: (builder) => { + builder + .addCase(registerUser.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(registerUser.fulfilled, (state, action) => { + state.isLoading = false; + }) + .addCase(registerUser.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + .addCase(loginUser.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(loginUser.fulfilled, (state, action) => { + state.isLoading = false; + state.user = action.payload.user; + state.token = action.payload.token; + state.isAuthenticated = true; + state.error = null; + }) + .addCase(loginUser.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + .addCase(logoutUser.pending, (state) => { + state.isLoading = true; + }) + .addCase(logoutUser.fulfilled, (state) => { + state.user = null; + state.token = null; + state.isAuthenticated = false; + state.isLoading = false; + state.error = null; + }) + .addCase(logoutUser.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + .addCase(verifyEmail.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(verifyEmail.fulfilled, (state) => { + state.isLoading = false; + state.error = null; + }) + .addCase(verifyEmail.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + .addCase(resendEmailVerification.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(resendEmailVerification.fulfilled, (state) => { + state.isLoading = false; + state.error = null; + }) + .addCase(resendEmailVerification.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + .addCase(forgotPassword.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(forgotPassword.fulfilled, (state) => { + state.isLoading = false; + }) + .addCase(forgotPassword.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }); + }, }); export const { setCredentials, clearCredentials, setAuthLoading, setAuthError } = authSlice.actions; diff --git a/src/features/auth/authThunks.ts b/src/features/auth/authThunks.ts index 52c095c..1f7682a 100644 --- a/src/features/auth/authThunks.ts +++ b/src/features/auth/authThunks.ts @@ -80,6 +80,20 @@ export const verifyEmail = createAsyncThunk( + 'auth/resendEmailVerification', + async (email, { rejectWithValue }) => { + try { + const response = await api.post('/auth/resend-verification', { email }); + toastSuccess('Verification email sent'); + return response.data; + } catch (err: any) { + toastError(err); + return rejectWithValue(err.response?.data || err.message); + } + } +); + export const forgotPassword = createAsyncThunk( 'auth/forgotPassword', async (email) => { diff --git a/src/pages/auth/VerifyEmailPage.jsx b/src/pages/auth/VerifyEmailPage.jsx new file mode 100644 index 0000000..b0f3f8b --- /dev/null +++ b/src/pages/auth/VerifyEmailPage.jsx @@ -0,0 +1,195 @@ +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useParams, Link } from "react-router-dom"; +import { Button } from "../../components/common/button"; +import Input from "../../components/common/input"; +import Spinner from "../../components/common/Spinner"; +import { verifyEmail, resendEmailVerification } from "../../features/auth/authThunks"; +import { selectAuthLoading, selectAuthError } from "../../features/auth/authSelectors"; + +/** + * VerifyEmailPage + * Handles email verification flow with token from URL params. + * Shows loading, success, or error states with appropriate actions. + */ +const VerifyEmailPage = () => { + const { token } = useParams(); + const dispatch = useDispatch(); + const isLoading = useSelector(selectAuthLoading); + const error = useSelector(selectAuthError); + const [verificationAttempted, setVerificationAttempted] = useState(false); + const [resendEmail, setResendEmail] = useState(""); + const [resendLoading, setResendLoading] = useState(false); + + useEffect(() => { + if (token && !verificationAttempted) { + dispatch(verifyEmail(token)); + setVerificationAttempted(true); + } + }, [token, dispatch, verificationAttempted]); + + const handleResendVerification = async (e) => { + e.preventDefault(); + if (!resendEmail || resendLoading) return; + + setResendLoading(true); + try { + await dispatch(resendEmailVerification(resendEmail)).unwrap(); + setResendEmail(""); + } catch (error) { + console.error("Resend verification failed:", error); + } finally { + setResendLoading(false); + } + }; + + const isSuccess = verificationAttempted && !isLoading && !error; + + return ( +
+
+ {/* Logo Section */} +
+
+ S +
+ + StellarAid + +
+ + {/* Content Section */} +
+

+ Email Verification +

+

+ {isLoading + ? "Verifying your email address..." + : isSuccess + ? "Your email has been successfully verified!" + : "We couldn't verify your email address."} +

+
+ + {isLoading && ( +
+ +
+ )} + + {isSuccess && ( +
+
+

+ Email verified! +

+

You can now log in to your account.

+
+ +
+ + + +
+
+ )} + + {error && !isLoading && ( +
+
+

+ Verification Failed +

+

+ {error?.message || "The verification link is invalid or has expired."} +

+
+ +
+
+ setResendEmail(e.target.value)} + autoComplete="email" + required + /> + + +
+
+
+ )} +
+
+ ); +}; + +export default VerifyEmailPage; \ No newline at end of file diff --git a/src/routes/AppRouter.jsx b/src/routes/AppRouter.jsx index 027afad..fdd4fff 100644 --- a/src/routes/AppRouter.jsx +++ b/src/routes/AppRouter.jsx @@ -11,6 +11,7 @@ import ErrorTest from '../components/common/ErrorTest'; import Login from '../pages/Login'; import Register from '../pages/Register'; import ForgotPasswordPage from '../pages/auth/ForgotPasswordPage'; +import VerifyEmailPage from '../pages/auth/VerifyEmailPage'; // Main pages — lazy-loaded for code splitting const Home = lazy(() => import('../pages/Home')); @@ -35,6 +36,7 @@ const AppRouter = () => { } /> } /> } /> + } /> } /> } /> } /> From 93658da7e938f442e9a2f3751cb5b72b842f0941 Mon Sep 17 00:00:00 2001 From: lynaDev2 Date: Thu, 5 Mar 2026 01:37:04 +0000 Subject: [PATCH 2/2] feat(auth): implement email verification flow UI with token handling and resend option --- src/features/auth/authSlice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/auth/authSlice.js b/src/features/auth/authSlice.js index b9573bd..340a623 100644 --- a/src/features/auth/authSlice.js +++ b/src/features/auth/authSlice.js @@ -41,7 +41,7 @@ const authSlice = createSlice({ state.isLoading = true; state.error = null; }) - .addCase(registerUser.fulfilled, (state, action) => { + .addCase(registerUser.fulfilled, (state) => { state.isLoading = false; }) .addCase(registerUser.rejected, (state, action) => {