From d4a4c4f799f29d328dbf5615890a5f76dbcf319f Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Thu, 26 Mar 2026 07:33:31 -0700 Subject: [PATCH 1/7] feat: implement email verification flow including resend and change email functionalities. --- app/auth/verify-email/VerifyEmail.tsx | 275 ++++++++++++++++++++++++++ app/auth/verify-email/page.tsx | 11 ++ lib/api/auth.ts | 26 ++- types/api.ts | 17 +- 4 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 app/auth/verify-email/VerifyEmail.tsx create mode 100644 app/auth/verify-email/page.tsx diff --git a/app/auth/verify-email/VerifyEmail.tsx b/app/auth/verify-email/VerifyEmail.tsx new file mode 100644 index 0000000..3505bb2 --- /dev/null +++ b/app/auth/verify-email/VerifyEmail.tsx @@ -0,0 +1,275 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Link from "next/link"; +import { useSearchParams, useRouter } from "next/navigation"; +import { Button, Input, useToast, Card, Spinner } from "@/components/ui"; +import { authApi } from "@/lib/api/auth"; +import { Mail, ArrowLeft, RefreshCw, CheckCircle2, AlertCircle, Edit2 } from "lucide-react"; + +const COOLDOWN_SECONDS = 60; + +const VerifyEmail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const toast = useToast(); + const token = searchParams.get("token"); + + const [email, setEmail] = useState(""); + const [isVerifying, setIsVerifying] = useState(!!token); + const [isResending, setIsResending] = useState(false); + const [isChangingEmail, setIsChangingEmail] = useState(false); + const [newEmail, setNewEmail] = useState(""); + const [cooldown, setCooldown] = useState(0); + const [status, setStatus] = useState<"pending" | "success" | "error">(token ? "pending" : "pending"); + const [errorMessage, setErrorMessage] = useState(""); + + // Handle auto-verification if token is present + useEffect(() => { + if (token) { + handleVerify(token); + } + }, [token]); + + // Cooldown timer + useEffect(() => { + if (cooldown > 0) { + const timer = setTimeout(() => setCooldown(cooldown - 1), 1000); + return () => clearTimeout(timer); + } + }, [cooldown]); + + const handleVerify = async (verificationToken: string) => { + setIsVerifying(true); + setStatus("pending"); + try { + await authApi.verifyEmail({ token: verificationToken }); + setStatus("success"); + toast.success("Email verified successfully! You can now log in."); + // Redirect to login after 3 seconds + setTimeout(() => { + router.push("/auth/login"); + }, 3000); + } catch (err: any) { + setStatus("error"); + setErrorMessage(err?.response?.data?.message || "Verification failed. The link may have expired or is invalid."); + toast.error("Verification failed"); + } finally { + setIsVerifying(false); + } + }; + + const handleResend = async () => { + if (cooldown > 0) return; + + setIsResending(true); + try { + // In a real app, you might want to get this from a state or context if available + // For now we use the email entered/changed + const targetEmail = email || "your email"; + await authApi.resendVerification({ email: targetEmail }); + toast.success(`Verification link sent to ${targetEmail}`); + setCooldown(COOLDOWN_SECONDS); + } catch (err: any) { + toast.error(err?.response?.data?.message || "Failed to resend verification link."); + } finally { + setIsResending(false); + } + }; + + const handleChangeEmail = async () => { + if (!newEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) { + toast.error("Please enter a valid email address"); + return; + } + + setIsResending(true); + try { + await authApi.changeEmail({ email: newEmail }); + setEmail(newEmail); + setIsChangingEmail(false); + toast.success("Email updated and new verification link sent!"); + setCooldown(COOLDOWN_SECONDS); + } catch (err: any) { + toast.error(err?.response?.data?.message || "Failed to update email."); + } finally { + setIsResending(false); + } + }; + + const renderContent = () => { + if (isVerifying) { + return ( +
+ +

Verifying your email address...

+
+ ); + } + + if (status === "success") { + return ( +
+
+
+ +
+
+
+

Email Verified!

+

Your account is now fully activated. We're redirecting you to the login page.

+
+ +
+ ); + } + + if (status === "error") { + return ( +
+
+
+ +
+
+
+

Verification Failed

+

{errorMessage}

+

Don't worry, you can request a new link below.

+
+
+ + +
+
+ ); + } + + // Default "pending" view (the initial view if no token or after successful resend) + return ( +
+
+
+
+ +
+
+

Check your email

+

+ We've sent a verification link to {email || "your email address"}. +

+
+ + {isChangingEmail ? ( +
+

Update Email Address

+ setNewEmail(e.target.value)} + placeholder="Enter new email" + autoFocus + /> +
+ + +
+
+ ) : ( +
+ + + +
+ )} + +
+ + + Back to Sign In + +
+
+ ); + }; + + return ( +
+ + {renderContent()} + + + {/* Visual background decoration */} +
+
+
+ ); +}; + +export default VerifyEmail; diff --git a/app/auth/verify-email/page.tsx b/app/auth/verify-email/page.tsx new file mode 100644 index 0000000..0c5c9d6 --- /dev/null +++ b/app/auth/verify-email/page.tsx @@ -0,0 +1,11 @@ +import VerifyEmail from './VerifyEmail'; +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Verify Email | StellarAid', + description: 'Verify your email address to activate your StellarAid account.', +}; + +export default function VerifyEmailPage() { + return ; +} diff --git a/lib/api/auth.ts b/lib/api/auth.ts index 72279a5..ed9a6c9 100644 --- a/lib/api/auth.ts +++ b/lib/api/auth.ts @@ -1,5 +1,5 @@ import { apiClient } from "./interceptors"; -import { LoginResponse, ApiResponse, RegisterRequest } from "@/types/api"; +import { LoginResponse, ApiResponse, RegisterRequest, VerifyEmailRequest, ResendEmailRequest, ChangeEmailRequest } from "@/types/api"; export const authApi = { login: async (credentials: any): Promise> => { @@ -49,4 +49,28 @@ export const authApi = { ); return response.data; }, + + verifyEmail: async (data: VerifyEmailRequest): Promise> => { + const response = await apiClient.post>( + "/auth/verify-email", + data, + ); + return response.data; + }, + + resendVerification: async (data: ResendEmailRequest): Promise> => { + const response = await apiClient.post>( + "/auth/resend-verification", + data, + ); + return response.data; + }, + + changeEmail: async (data: ChangeEmailRequest): Promise> => { + const response = await apiClient.patch>( + "/auth/change-email", + data, + ); + return response.data; + }, }; diff --git a/types/api.ts b/types/api.ts index 7a0b3e0..c657c25 100644 --- a/types/api.ts +++ b/types/api.ts @@ -20,8 +20,21 @@ export interface LoginResponse { export interface RegisterRequest { email: string; - name: string; - password?: string; // Optional if using wallet-only or social + role: "donor" | "creator"; + password?: string; + confirmPassword?: string; +} + +export interface VerifyEmailRequest { + token: string; +} + +export interface ResendEmailRequest { + email: string; +} + +export interface ChangeEmailRequest { + email: string; } // Project Types From 10113058078008b668510eaf23ae91d785762deb Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Thu, 26 Mar 2026 13:38:33 -0700 Subject: [PATCH 2/7] fix --- app/auth/verify-email/VerifyEmail.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/auth/verify-email/VerifyEmail.tsx b/app/auth/verify-email/VerifyEmail.tsx index 3505bb2..6349371 100644 --- a/app/auth/verify-email/VerifyEmail.tsx +++ b/app/auth/verify-email/VerifyEmail.tsx @@ -29,7 +29,7 @@ const VerifyEmail = () => { if (token) { handleVerify(token); } - }, [token]); + }, [token, handleVerify]); // Cooldown timer useEffect(() => { @@ -39,7 +39,7 @@ const VerifyEmail = () => { } }, [cooldown]); - const handleVerify = async (verificationToken: string) => { + const handleVerify = useCallback(async (verificationToken: string) => { setIsVerifying(true); setStatus("pending"); try { @@ -57,7 +57,7 @@ const VerifyEmail = () => { } finally { setIsVerifying(false); } - }; + }, [router, toast]); const handleResend = async () => { if (cooldown > 0) return; @@ -117,7 +117,7 @@ const VerifyEmail = () => {

Email Verified!

-

Your account is now fully activated. We're redirecting you to the login page.

+

Your account is now fully activated. We're redirecting you to the login page.

Check your email

- We've sent a verification link to {email || "your email address"}. + We've sent a verification link to {email || "your email address"}.

From 3078e7233fb420541c533ce22b631c7dd288cec4 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Thu, 26 Mar 2026 13:42:16 -0700 Subject: [PATCH 3/7] fix --- lib/api/auth.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/api/auth.ts b/lib/api/auth.ts index ed9a6c9..cb012a4 100644 --- a/lib/api/auth.ts +++ b/lib/api/auth.ts @@ -1,5 +1,12 @@ import { apiClient } from "./interceptors"; -import { LoginResponse, ApiResponse, RegisterRequest, VerifyEmailRequest, ResendEmailRequest, ChangeEmailRequest } from "@/types/api"; +import { + LoginResponse, + ApiResponse, + RegisterRequest, + VerifyEmailRequest, + ResendEmailRequest, + ChangeEmailRequest +} from "@/types/api"; export const authApi = { login: async (credentials: any): Promise> => { @@ -10,9 +17,7 @@ export const authApi = { return response.data; }, - register: async ( - data: RegisterRequest, - ): Promise> => { + register: async (data: RegisterRequest): Promise> => { const response = await apiClient.post>( "/auth/register", data, @@ -29,9 +34,7 @@ export const authApi = { return response.data; }, - forgotPassword: async (data: { - email: string; - }): Promise> => { + forgotPassword: async (data: { email: string }): Promise> => { const response = await apiClient.post>( "/users/forgot-password", data, @@ -39,17 +42,14 @@ export const authApi = { return response.data; }, - resetPassword: async (data: { - token: string; - password: string; - }): Promise> => { + resetPassword: async (data: { token: string; password: string }): Promise> => { const response = await apiClient.post>( "/users/reset-password", data, ); return response.data; }, - + verifyEmail: async (data: VerifyEmailRequest): Promise> => { const response = await apiClient.post>( "/auth/verify-email", From 0cba3b14ba1c9dff04e28f419a7a9e3b555a8e5e Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Thu, 26 Mar 2026 13:43:49 -0700 Subject: [PATCH 4/7] fix --- components/ProfileDropdown.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index cd3c041..b1754bd 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -4,6 +4,7 @@ import { useState, useRef, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { User, Settings, LayoutDashboard, LogOut, ChevronDown } from 'lucide-react'; +import Image from 'next/image'; import { useAuthStore } from '@/store/authStore'; export default function ProfileDropdown() { @@ -108,10 +109,13 @@ export default function ProfileDropdown() { {/* User Avatar */}
{user?.avatar ? ( - {user.name} ) : ( {user?.name ? getUserInitials(user.name) : 'U'} From 25d1554fac657b6c428d80fed9e2137ea4d77890 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Thu, 26 Mar 2026 13:45:51 -0700 Subject: [PATCH 5/7] fix --- lib/api/auth.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/api/auth.ts b/lib/api/auth.ts index 17465f8..2681978 100644 --- a/lib/api/auth.ts +++ b/lib/api/auth.ts @@ -70,6 +70,9 @@ export const authApi = { const response = await apiClient.patch>( "/auth/change-email", data, + ); + return response.data; + }, refreshToken: async (): Promise> => { const response = await apiClient.post>( From e3b4beb82c890569741469409a73a600170343ba Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Thu, 26 Mar 2026 13:50:10 -0700 Subject: [PATCH 6/7] fix --- app/auth/verify-email/VerifyEmail.tsx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/app/auth/verify-email/VerifyEmail.tsx b/app/auth/verify-email/VerifyEmail.tsx index 6349371..5b378b7 100644 --- a/app/auth/verify-email/VerifyEmail.tsx +++ b/app/auth/verify-email/VerifyEmail.tsx @@ -24,21 +24,6 @@ const VerifyEmail = () => { const [status, setStatus] = useState<"pending" | "success" | "error">(token ? "pending" : "pending"); const [errorMessage, setErrorMessage] = useState(""); - // Handle auto-verification if token is present - useEffect(() => { - if (token) { - handleVerify(token); - } - }, [token, handleVerify]); - - // Cooldown timer - useEffect(() => { - if (cooldown > 0) { - const timer = setTimeout(() => setCooldown(cooldown - 1), 1000); - return () => clearTimeout(timer); - } - }, [cooldown]); - const handleVerify = useCallback(async (verificationToken: string) => { setIsVerifying(true); setStatus("pending"); @@ -59,6 +44,13 @@ const VerifyEmail = () => { } }, [router, toast]); + // Handle auto-verification if token is present + useEffect(() => { + if (token) { + handleVerify(token); + } + }, [token, handleVerify]); + const handleResend = async () => { if (cooldown > 0) return; From b745418ae8c37dd21d80afe1f55a35c8450a6d22 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Thu, 26 Mar 2026 13:58:15 -0700 Subject: [PATCH 7/7] fix --- app/auth/verify-email/VerifyEmail.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/auth/verify-email/VerifyEmail.tsx b/app/auth/verify-email/VerifyEmail.tsx index 5b378b7..596a3d9 100644 --- a/app/auth/verify-email/VerifyEmail.tsx +++ b/app/auth/verify-email/VerifyEmail.tsx @@ -51,6 +51,14 @@ const VerifyEmail = () => { } }, [token, handleVerify]); + // Cooldown timer + useEffect(() => { + if (cooldown > 0) { + const timer = setTimeout(() => setCooldown(cooldown - 1), 1000); + return () => clearTimeout(timer); + } + }, [cooldown]); + const handleResend = async () => { if (cooldown > 0) return;