|
|
|
@ -1,7 +1,7 @@ |
|
|
|
'use client' |
|
|
|
|
|
|
|
import { useState, useEffect } from 'react' |
|
|
|
import { useSearchParams } from 'next/navigation' |
|
|
|
import { useSearchParams, useRouter } from 'next/navigation' |
|
|
|
import Link from 'next/link' |
|
|
|
import { Button } from '@/components/ui/button' |
|
|
|
import { |
|
|
|
@ -14,11 +14,12 @@ import { |
|
|
|
} from '@/components/ui/card' |
|
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' |
|
|
|
import { Loader2, CheckCircle, XCircle, Mail, ArrowRight } from 'lucide-react' |
|
|
|
import { useMutation } from '@tanstack/react-query' |
|
|
|
import { useMutation, useQuery } from '@tanstack/react-query' |
|
|
|
import httpBrowserClient from '@/lib/httpBrowserClient' |
|
|
|
import { ApiEndpoints } from '@/config/api' |
|
|
|
import { Routes } from '@/config/routes' |
|
|
|
|
|
|
|
// Reusable components
|
|
|
|
const ErrorAlert = ({ message }: { message: string }) => ( |
|
|
|
<Alert |
|
|
|
variant='destructive' |
|
|
|
@ -30,258 +31,291 @@ const ErrorAlert = ({ message }: { message: string }) => ( |
|
|
|
</Alert> |
|
|
|
) |
|
|
|
|
|
|
|
const SendVerificationButton = ({ |
|
|
|
onClick, |
|
|
|
isLoading, |
|
|
|
hasVerificationCode, |
|
|
|
}: { |
|
|
|
onClick: () => void |
|
|
|
isLoading: boolean |
|
|
|
hasVerificationCode: boolean |
|
|
|
}) => ( |
|
|
|
<Button |
|
|
|
className='w-full text-lg py-6' |
|
|
|
onClick={onClick} |
|
|
|
disabled={isLoading} |
|
|
|
> |
|
|
|
{isLoading ? ( |
|
|
|
<Loader2 className='mr-2 h-5 w-5 animate-spin' /> |
|
|
|
) : ( |
|
|
|
<Mail className='mr-2 h-5 w-5' /> |
|
|
|
)} |
|
|
|
{hasVerificationCode |
|
|
|
? 'Resend Verification Email' |
|
|
|
: 'Send Verification Email'} |
|
|
|
const SuccessAlert = ({ title, message }: { title: string; message: string }) => ( |
|
|
|
<Alert className='bg-green-50 text-green-700 border-green-200'> |
|
|
|
<CheckCircle className='h-5 w-5 text-green-600' /> |
|
|
|
<AlertTitle className='text-lg font-semibold'>{title}</AlertTitle> |
|
|
|
<AlertDescription>{message}</AlertDescription> |
|
|
|
</Alert> |
|
|
|
) |
|
|
|
|
|
|
|
const InfoAlert = ({ title, message }: { title: string; message: string }) => ( |
|
|
|
<Alert className='bg-blue-50 text-blue-700 border-blue-200'> |
|
|
|
<Mail className='h-5 w-5 text-blue-600' /> |
|
|
|
<AlertTitle className='text-lg font-semibold'>{title}</AlertTitle> |
|
|
|
<AlertDescription>{message}</AlertDescription> |
|
|
|
</Alert> |
|
|
|
) |
|
|
|
|
|
|
|
const LoadingSpinner = () => ( |
|
|
|
<div className='flex justify-center py-6'> |
|
|
|
<Loader2 className='h-10 w-10 animate-spin text-primary' /> |
|
|
|
</div> |
|
|
|
) |
|
|
|
|
|
|
|
const DashboardButton = () => ( |
|
|
|
<Button className='w-full py-5 mt-2' asChild> |
|
|
|
<Link href={Routes.dashboard}> |
|
|
|
Go to Dashboard |
|
|
|
<ArrowRight className='ml-2 h-5 w-5' /> |
|
|
|
</Link> |
|
|
|
</Button> |
|
|
|
) |
|
|
|
|
|
|
|
const LoginButton = () => ( |
|
|
|
<Button className='w-full py-5 mt-2' asChild> |
|
|
|
<Link href='/login'> |
|
|
|
Go to Login |
|
|
|
<ArrowRight className='ml-2 h-5 w-5' /> |
|
|
|
</Link> |
|
|
|
</Button> |
|
|
|
) |
|
|
|
|
|
|
|
const SendVerificationEmail = () => { |
|
|
|
export default function VerifyEmailPage() { |
|
|
|
const router = useRouter() |
|
|
|
const searchParams = useSearchParams() |
|
|
|
const userId = searchParams.get('userId') |
|
|
|
const verificationCode = searchParams.get('verificationCode') |
|
|
|
const verificationEmailSent = searchParams.get('verificationEmailSent') |
|
|
|
|
|
|
|
const [successMessage, setSuccessMessage] = useState<string>('') |
|
|
|
const [error, setError] = useState<string>('') |
|
|
|
const [errorMessage, setErrorMessage] = useState<string>('') |
|
|
|
|
|
|
|
// Check user authentication and email verification status
|
|
|
|
const { |
|
|
|
data: whoAmIData, |
|
|
|
isPending: isCheckingAuth, |
|
|
|
isError: isAuthError |
|
|
|
} = useQuery({ |
|
|
|
queryKey: ['whoAmI'], |
|
|
|
queryFn: () => httpBrowserClient.get(ApiEndpoints.auth.whoAmI()), |
|
|
|
retry: 1, |
|
|
|
}) |
|
|
|
|
|
|
|
const user = whoAmIData?.data?.data |
|
|
|
const isEmailVerified = !!user?.emailVerifiedAt |
|
|
|
const isLoggedIn = !isAuthError && !!user |
|
|
|
|
|
|
|
const { |
|
|
|
mutate: sendVerificationEmailMutation, |
|
|
|
isPending: isSendingVerificationEmail, |
|
|
|
// Verify email mutation
|
|
|
|
const { |
|
|
|
mutate: verifyEmail, |
|
|
|
isPending: isVerifying |
|
|
|
} = useMutation({ |
|
|
|
mutationFn: () => |
|
|
|
httpBrowserClient.post( |
|
|
|
ApiEndpoints.auth.sendEmailVerificationEmail(), |
|
|
|
{} |
|
|
|
), |
|
|
|
mutationFn: () => httpBrowserClient.post('/auth/verify-email', { |
|
|
|
userId, |
|
|
|
verificationCode, |
|
|
|
}), |
|
|
|
onSuccess: () => { |
|
|
|
setSuccessMessage('Verification email has been sent to your inbox') |
|
|
|
setSuccessMessage('Your email has been successfully verified') |
|
|
|
setErrorMessage('') |
|
|
|
}, |
|
|
|
onError: (error: any) => { |
|
|
|
setError(error.message || 'Failed to send verification email') |
|
|
|
setErrorMessage(error.message || 'Failed to verify email') |
|
|
|
}, |
|
|
|
}) |
|
|
|
|
|
|
|
const renderContent = () => { |
|
|
|
if (successMessage) |
|
|
|
return ( |
|
|
|
<Alert |
|
|
|
variant='default' |
|
|
|
className='bg-blue-50 text-blue-700 border-blue-200' |
|
|
|
> |
|
|
|
<Mail className='h-5 w-5 text-blue-600' /> |
|
|
|
<AlertTitle className='text-lg font-semibold'>Email Sent</AlertTitle> |
|
|
|
<AlertDescription>{successMessage}</AlertDescription> |
|
|
|
</Alert> |
|
|
|
) |
|
|
|
if (error) return <ErrorAlert message={error} /> |
|
|
|
return null |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<Card className='w-full max-w-md'> |
|
|
|
<CardHeader> |
|
|
|
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle> |
|
|
|
<CardDescription> |
|
|
|
Send a verification email to verify your account |
|
|
|
</CardDescription> |
|
|
|
</CardHeader> |
|
|
|
<CardContent className='space-y-4'>{renderContent()}</CardContent> |
|
|
|
<CardFooter> |
|
|
|
<SendVerificationButton |
|
|
|
onClick={() => sendVerificationEmailMutation()} |
|
|
|
isLoading={isSendingVerificationEmail} |
|
|
|
hasVerificationCode={false} |
|
|
|
/> |
|
|
|
</CardFooter> |
|
|
|
</Card> |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
const VerifyEmail = ({ |
|
|
|
userId, |
|
|
|
verificationCode, |
|
|
|
}: { |
|
|
|
userId: string |
|
|
|
verificationCode: string |
|
|
|
}) => { |
|
|
|
const [successMessage, setSuccessMessage] = useState<string>('') |
|
|
|
const [error, setError] = useState<string>('') |
|
|
|
const [isVerified, setIsVerified] = useState(false) |
|
|
|
|
|
|
|
const { mutate: verifyEmailMutation, isPending: isVerifyingEmail } = |
|
|
|
useMutation({ |
|
|
|
mutationFn: () => |
|
|
|
httpBrowserClient.post('/auth/verify-email', { |
|
|
|
userId, |
|
|
|
verificationCode, |
|
|
|
}), |
|
|
|
onSuccess: () => { |
|
|
|
setIsVerified(true) |
|
|
|
setSuccessMessage('Your email has been successfully verified') |
|
|
|
}, |
|
|
|
onError: (error: any) => { |
|
|
|
setError(error.message || 'Failed to verify email') |
|
|
|
}, |
|
|
|
}) |
|
|
|
// Send verification email mutation
|
|
|
|
const { |
|
|
|
mutate: sendVerificationEmail, |
|
|
|
isPending: isSending |
|
|
|
} = useMutation({ |
|
|
|
mutationFn: () => httpBrowserClient.post( |
|
|
|
ApiEndpoints.auth.sendEmailVerificationEmail(), |
|
|
|
{} |
|
|
|
), |
|
|
|
onSuccess: () => { |
|
|
|
if (!verificationEmailSent) { |
|
|
|
router.push('/verify-email?verificationEmailSent=true') |
|
|
|
} else { |
|
|
|
setSuccessMessage('Verification email has been sent to your inbox') |
|
|
|
setErrorMessage('') |
|
|
|
} |
|
|
|
}, |
|
|
|
onError: (error: any) => { |
|
|
|
setErrorMessage(error.message || 'Failed to send verification email') |
|
|
|
}, |
|
|
|
}) |
|
|
|
|
|
|
|
// Handle verification when code is provided
|
|
|
|
useEffect(() => { |
|
|
|
verifyEmailMutation() |
|
|
|
}, [verifyEmailMutation]) |
|
|
|
if (userId && verificationCode && !isVerifying && !successMessage && !errorMessage) { |
|
|
|
if (isEmailVerified) { |
|
|
|
setSuccessMessage('Your email has already been verified') |
|
|
|
} else if (!isCheckingAuth) { |
|
|
|
verifyEmail() |
|
|
|
} |
|
|
|
} |
|
|
|
}, [userId, verificationCode, isCheckingAuth, isEmailVerified, isVerifying, successMessage, errorMessage, verifyEmail]) |
|
|
|
|
|
|
|
// Render content based on current state
|
|
|
|
const renderContent = () => { |
|
|
|
if (isVerifyingEmail) |
|
|
|
// Show loading state
|
|
|
|
if (isCheckingAuth) { |
|
|
|
return ( |
|
|
|
<div className='flex justify-center py-8'> |
|
|
|
<Loader2 className='h-12 w-12 animate-spin text-primary' /> |
|
|
|
</div> |
|
|
|
<> |
|
|
|
<CardHeader> |
|
|
|
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle> |
|
|
|
<CardDescription>Checking verification status...</CardDescription> |
|
|
|
</CardHeader> |
|
|
|
<CardContent> |
|
|
|
<LoadingSpinner /> |
|
|
|
</CardContent> |
|
|
|
</> |
|
|
|
) |
|
|
|
if (isVerified) |
|
|
|
} |
|
|
|
|
|
|
|
// Handle verification process
|
|
|
|
if (userId && verificationCode) { |
|
|
|
return ( |
|
|
|
<Alert |
|
|
|
variant='default' |
|
|
|
className='bg-green-50 text-green-700 border-green-200' |
|
|
|
> |
|
|
|
<CheckCircle className='h-5 w-5 text-green-600' /> |
|
|
|
<AlertTitle className='text-lg font-semibold'>Success</AlertTitle> |
|
|
|
<AlertDescription> |
|
|
|
<div className='flex flex-col gap-2'> |
|
|
|
<div>{successMessage}</div> |
|
|
|
<Link href={Routes.dashboard} className='font-medium underline'> |
|
|
|
Go to Dashboard |
|
|
|
</Link> |
|
|
|
</div> |
|
|
|
</AlertDescription> |
|
|
|
</Alert> |
|
|
|
<> |
|
|
|
<CardHeader> |
|
|
|
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle> |
|
|
|
<CardDescription> |
|
|
|
{isVerifying ? 'Verifying your email address...' : 'Email Verification Status'} |
|
|
|
</CardDescription> |
|
|
|
</CardHeader> |
|
|
|
<CardContent className='space-y-4'> |
|
|
|
{isVerifying ? ( |
|
|
|
<LoadingSpinner /> |
|
|
|
) : successMessage ? ( |
|
|
|
<SuccessAlert title="Success" message={successMessage} /> |
|
|
|
) : errorMessage ? ( |
|
|
|
<ErrorAlert message={errorMessage} /> |
|
|
|
) : null} |
|
|
|
</CardContent> |
|
|
|
<CardFooter> |
|
|
|
{successMessage && <DashboardButton />} |
|
|
|
</CardFooter> |
|
|
|
</> |
|
|
|
) |
|
|
|
if (error) return <ErrorAlert message={error} /> |
|
|
|
return null |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<Card className='w-full max-w-md'> |
|
|
|
<CardHeader> |
|
|
|
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle> |
|
|
|
<CardDescription>Verifying your email address...</CardDescription> |
|
|
|
</CardHeader> |
|
|
|
<CardContent className='space-y-4'>{renderContent()}</CardContent> |
|
|
|
<CardFooter className='flex flex-col gap-4'> |
|
|
|
{isVerified && ( |
|
|
|
<Button className='w-full text-lg py-6' asChild> |
|
|
|
<Link href='/dashboard'> |
|
|
|
Go to Dashboard |
|
|
|
<ArrowRight className='ml-2 h-5 w-5' /> |
|
|
|
</Link> |
|
|
|
</Button> |
|
|
|
)} |
|
|
|
</CardFooter> |
|
|
|
</Card> |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
const CheckEmailPrompt = () => { |
|
|
|
const { |
|
|
|
mutate: sendVerificationEmailMutation, |
|
|
|
isPending, |
|
|
|
isError, |
|
|
|
isSuccess, |
|
|
|
} = useMutation({ |
|
|
|
mutationFn: () => |
|
|
|
httpBrowserClient.post( |
|
|
|
ApiEndpoints.auth.sendEmailVerificationEmail(), |
|
|
|
{} |
|
|
|
), |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<Card className='w-full max-w-md'> |
|
|
|
<CardHeader> |
|
|
|
<CardTitle className='text-2xl font-bold'>Check your email</CardTitle> |
|
|
|
<CardDescription> |
|
|
|
We've sent you a verification email. Please check your inbox and click |
|
|
|
the link to verify your account. |
|
|
|
</CardDescription> |
|
|
|
</CardHeader> |
|
|
|
<CardContent className='space-y-4'> |
|
|
|
{isSuccess && ( |
|
|
|
<Alert className='bg-green-50 text-green-700 border-green-200'> |
|
|
|
<CheckCircle className='h-5 w-5 text-green-600' /> |
|
|
|
<AlertTitle className='text-lg font-semibold'> |
|
|
|
Email Sent |
|
|
|
</AlertTitle> |
|
|
|
<AlertDescription> |
|
|
|
A new verification email has been sent to your inbox |
|
|
|
</AlertDescription> |
|
|
|
</Alert> |
|
|
|
)} |
|
|
|
{isError && ( |
|
|
|
<Alert |
|
|
|
variant='destructive' |
|
|
|
className='bg-red-50 text-red-700 border-red-200' |
|
|
|
> |
|
|
|
<XCircle className='h-5 w-5 text-red-600' /> |
|
|
|
<AlertTitle className='text-lg font-semibold'>Error</AlertTitle> |
|
|
|
<AlertDescription> |
|
|
|
Failed to resend verification email |
|
|
|
</AlertDescription> |
|
|
|
</Alert> |
|
|
|
)} |
|
|
|
</CardContent> |
|
|
|
<CardFooter className='flex flex-col gap-4'> |
|
|
|
<div className='flex items-center gap-2 justify-center w-full'> |
|
|
|
<span className='text-sm text-gray-600'> |
|
|
|
Didn't receive the email? |
|
|
|
</span> |
|
|
|
<Button |
|
|
|
variant='link' |
|
|
|
onClick={() => sendVerificationEmailMutation()} |
|
|
|
disabled={isPending} |
|
|
|
className='text-sm p-0 h-auto font-semibold' |
|
|
|
> |
|
|
|
{isPending ? ( |
|
|
|
<> |
|
|
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' /> |
|
|
|
Sending... |
|
|
|
</> |
|
|
|
// Handle "check your email" state
|
|
|
|
if (verificationEmailSent) { |
|
|
|
return ( |
|
|
|
<> |
|
|
|
<CardHeader> |
|
|
|
<CardTitle className='text-2xl font-bold'>Check Your Email</CardTitle> |
|
|
|
<CardDescription> |
|
|
|
We've sent you a verification email. Please check your inbox and click |
|
|
|
the link to verify your account. |
|
|
|
</CardDescription> |
|
|
|
</CardHeader> |
|
|
|
<CardContent className='space-y-4'> |
|
|
|
{successMessage && ( |
|
|
|
<InfoAlert title="Email Sent" message={successMessage} /> |
|
|
|
)} |
|
|
|
{errorMessage && ( |
|
|
|
<ErrorAlert message={errorMessage} /> |
|
|
|
)} |
|
|
|
{isEmailVerified && ( |
|
|
|
<SuccessAlert |
|
|
|
title="Already Verified" |
|
|
|
message="Your email has already been verified" |
|
|
|
/> |
|
|
|
)} |
|
|
|
</CardContent> |
|
|
|
<CardFooter className='flex flex-col gap-3'> |
|
|
|
{isEmailVerified ? ( |
|
|
|
<DashboardButton /> |
|
|
|
) : ( |
|
|
|
'Click to resend' |
|
|
|
<div className='flex items-center gap-2 justify-center w-full'> |
|
|
|
<span className='text-sm text-gray-600'> |
|
|
|
Didn't receive the email? |
|
|
|
</span> |
|
|
|
<Button |
|
|
|
variant='link' |
|
|
|
onClick={() => sendVerificationEmail()} |
|
|
|
disabled={isSending} |
|
|
|
className='text-sm p-0 h-auto font-semibold' |
|
|
|
> |
|
|
|
{isSending ? ( |
|
|
|
<> |
|
|
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' /> |
|
|
|
Sending... |
|
|
|
</> |
|
|
|
) : ( |
|
|
|
'Click to resend' |
|
|
|
)} |
|
|
|
</Button> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</Button> |
|
|
|
</div> |
|
|
|
</CardFooter> |
|
|
|
</Card> |
|
|
|
) |
|
|
|
} |
|
|
|
</CardFooter> |
|
|
|
</> |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
export default function VerifyEmailPage() { |
|
|
|
const searchParams = useSearchParams() |
|
|
|
const userId = searchParams.get('userId') |
|
|
|
const verificationCode = searchParams.get('verificationCode') |
|
|
|
const verificationEmailSent = searchParams.get('verificationEmailSent') |
|
|
|
// Handle "send verification email" state
|
|
|
|
return ( |
|
|
|
<> |
|
|
|
<CardHeader> |
|
|
|
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle> |
|
|
|
<CardDescription> |
|
|
|
{isLoggedIn |
|
|
|
? isEmailVerified |
|
|
|
? 'Your email is already verified' |
|
|
|
: 'Verify your email address to access all features' |
|
|
|
: 'You need to be logged in to verify your email' |
|
|
|
} |
|
|
|
</CardDescription> |
|
|
|
</CardHeader> |
|
|
|
<CardContent className='space-y-4'> |
|
|
|
{successMessage && ( |
|
|
|
<InfoAlert title="Email Sent" message={successMessage} /> |
|
|
|
)} |
|
|
|
{errorMessage && ( |
|
|
|
<ErrorAlert message={errorMessage} /> |
|
|
|
)} |
|
|
|
{isEmailVerified && ( |
|
|
|
<SuccessAlert |
|
|
|
title="Already Verified" |
|
|
|
message="Your email has already been verified" |
|
|
|
/> |
|
|
|
)} |
|
|
|
{!isLoggedIn && ( |
|
|
|
<Alert |
|
|
|
variant='destructive' |
|
|
|
className='bg-red-50 text-red-700 border-red-200' |
|
|
|
> |
|
|
|
<XCircle className='h-5 w-5 text-red-600' /> |
|
|
|
<AlertTitle className='text-lg font-semibold'>Not Logged In</AlertTitle> |
|
|
|
<AlertDescription> |
|
|
|
You need to be logged in to verify your email |
|
|
|
</AlertDescription> |
|
|
|
</Alert> |
|
|
|
)} |
|
|
|
</CardContent> |
|
|
|
<CardFooter> |
|
|
|
{isLoggedIn ? ( |
|
|
|
isEmailVerified ? ( |
|
|
|
<DashboardButton /> |
|
|
|
) : ( |
|
|
|
<Button |
|
|
|
className='w-full py-5' |
|
|
|
onClick={() => sendVerificationEmail()} |
|
|
|
disabled={isSending} |
|
|
|
> |
|
|
|
{isSending ? ( |
|
|
|
<Loader2 className='mr-2 h-5 w-5 animate-spin' /> |
|
|
|
) : ( |
|
|
|
<Mail className='mr-2 h-5 w-5' /> |
|
|
|
)} |
|
|
|
Send Verification Email |
|
|
|
</Button> |
|
|
|
) |
|
|
|
) : ( |
|
|
|
<LoginButton /> |
|
|
|
)} |
|
|
|
</CardFooter> |
|
|
|
</> |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className='flex min-h-screen items-center justify-center bg-gray-100 dark:bg-gray-900 p-4'> |
|
|
|
{userId && verificationCode ? ( |
|
|
|
<VerifyEmail userId={userId} verificationCode={verificationCode} /> |
|
|
|
) : verificationEmailSent ? ( |
|
|
|
<CheckEmailPrompt /> |
|
|
|
) : ( |
|
|
|
<SendVerificationEmail /> |
|
|
|
)} |
|
|
|
<Card className='w-full max-w-md shadow-lg border-gray-200 dark:border-gray-800'> |
|
|
|
{renderContent()} |
|
|
|
</Card> |
|
|
|
</div> |
|
|
|
) |
|
|
|
} |