Browse Source

refactor(web): refactor email verification page

pull/58/head
isra el 12 months ago
parent
commit
6ba5767aba
  1. 488
      web/app/(app)/(auth)/verify-email/page.tsx

488
web/app/(app)/(auth)/verify-email/page.tsx

@ -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>
)
}
Loading…
Cancel
Save