344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { API_PATHS } from '../types/api';
|
|
|
|
interface EmailVerificationProps {
|
|
email?: string;
|
|
onVerified?: () => void;
|
|
onVerificationSent?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export default function EmailVerification({
|
|
email,
|
|
onVerified,
|
|
onVerificationSent,
|
|
className = '',
|
|
}: EmailVerificationProps) {
|
|
const [isVerified, setIsVerified] = useState<boolean | null>(null);
|
|
const [isSending, setIsSending] = useState(false);
|
|
const [isResending, setIsResending] = useState(false);
|
|
const [countdown, setCountdown] = useState(0);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
|
|
// Check verification status on mount
|
|
useEffect(() => {
|
|
checkVerificationStatus();
|
|
}, []);
|
|
|
|
// Handle countdown for resend button
|
|
useEffect(() => {
|
|
if (countdown > 0) {
|
|
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [countdown]);
|
|
|
|
// Check email verification status
|
|
const checkVerificationStatus = async () => {
|
|
try {
|
|
const response = await fetch('/api/auth/email-status', {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// If endpoint doesn't exist, assume unverified
|
|
setIsVerified(false);
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
setIsVerified(data.isVerified);
|
|
} catch (err) {
|
|
// Assume unverified if we can't check
|
|
setIsVerified(false);
|
|
}
|
|
};
|
|
|
|
// Send verification email
|
|
const sendVerificationEmail = async (isResend = false) => {
|
|
try {
|
|
if (isResend) {
|
|
setIsResending(true);
|
|
} else {
|
|
setIsSending(true);
|
|
}
|
|
|
|
setError(null);
|
|
setMessage(null);
|
|
|
|
const response = await fetch(API_PATHS.AUTH_VERIFY_EMAIL, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
email,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to send verification email');
|
|
}
|
|
|
|
setMessage('Verification email sent! Please check your inbox.');
|
|
onVerificationSent?.();
|
|
|
|
// Start countdown for resend
|
|
setCountdown(60);
|
|
|
|
if (!isResend) {
|
|
// Poll for verification status
|
|
const pollInterval = setInterval(async () => {
|
|
try {
|
|
const statusResponse = await fetch('/api/auth/email-status', {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (statusResponse.ok) {
|
|
const data = await statusResponse.json();
|
|
if (data.isVerified) {
|
|
setIsVerified(true);
|
|
clearInterval(pollInterval);
|
|
onVerified?.();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Continue polling
|
|
}
|
|
}, 5000);
|
|
|
|
// Stop polling after 5 minutes
|
|
setTimeout(() => clearInterval(pollInterval), 300000);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to send verification email');
|
|
} finally {
|
|
setIsSending(false);
|
|
setIsResending(false);
|
|
}
|
|
};
|
|
|
|
// Handle manual verification (user enters code)
|
|
const handleManualVerification = async (code: string) => {
|
|
try {
|
|
setError(null);
|
|
setMessage(null);
|
|
|
|
const response = await fetch(API_PATHS.AUTH_CONFIRM_EMAIL, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
code,
|
|
email,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || 'Invalid verification code');
|
|
}
|
|
|
|
setIsVerified(true);
|
|
setMessage('Email verified successfully!');
|
|
onVerified?.();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Invalid verification code');
|
|
}
|
|
};
|
|
|
|
// Loading state
|
|
if (isVerified === null) {
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow p-6 ${className}`}>
|
|
<div className="flex items-center justify-center py-4">
|
|
<svg className="animate-spin h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24">
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Verified state
|
|
if (isVerified) {
|
|
return (
|
|
<div className={`bg-green-50 border border-green-200 rounded-lg p-6 ${className}`}>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex-shrink-0">
|
|
<svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="font-medium text-green-900">Email verified</h3>
|
|
<p className="text-sm text-green-700 mt-1">
|
|
Your email address {email} has been verified successfully.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Unverified state
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow-lg ${className}`}>
|
|
<div className="p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="flex-shrink-0">
|
|
<svg className="w-6 h-6 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="font-medium text-gray-900">Email verification required</h3>
|
|
<p className="text-sm text-gray-600">
|
|
Please verify your email address {email ? `(${email})` : ''} to access all features.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error message */}
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
<p className="text-sm text-red-600">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Success message */}
|
|
{message && (
|
|
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg">
|
|
<p className="text-sm text-green-600">{message}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Send verification button */}
|
|
{!isSending && (
|
|
<button
|
|
onClick={() => sendVerificationEmail(false)}
|
|
className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
|
|
>
|
|
Send verification email
|
|
</button>
|
|
)}
|
|
|
|
{/* Loading state */}
|
|
{isSending && (
|
|
<div className="w-full py-2 px-4 bg-blue-600 text-white font-medium rounded-lg flex items-center justify-center">
|
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
Sending...
|
|
</div>
|
|
)}
|
|
|
|
{/* Resend option */}
|
|
{countdown > 0 ? (
|
|
<p className="mt-4 text-center text-sm text-gray-500">
|
|
Resend available in {countdown} seconds
|
|
</p>
|
|
) : (
|
|
<button
|
|
onClick={() => sendVerificationEmail(true)}
|
|
disabled={isResending}
|
|
className="mt-4 w-full py-2 px-4 text-blue-600 hover:text-blue-700 font-medium disabled:opacity-50 transition-colors"
|
|
>
|
|
{isResending ? 'Resending...' : 'Resend verification email'}
|
|
</button>
|
|
)}
|
|
|
|
{/* Manual verification option */}
|
|
<div className="mt-6 pt-6 border-t border-gray-200">
|
|
<ManualVerificationForm onVerify={handleManualVerification} />
|
|
</div>
|
|
|
|
{/* Help text */}
|
|
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
|
<h4 className="font-medium text-gray-900 mb-2">Didn't receive the email?</h4>
|
|
<ul className="text-sm text-gray-600 space-y-1">
|
|
<li>• Check your spam or junk folder</li>
|
|
<li>• Make sure the email address is correct</li>
|
|
<li>• Wait a few minutes for delivery</li>
|
|
<li>• Try clicking the resend button above</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Manual verification form component
|
|
function ManualVerificationForm({ onVerify }: { onVerify: (code: string) => void }) {
|
|
const [code, setCode] = useState('');
|
|
const [isVerifying, setIsVerifying] = useState(false);
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (code.trim()) {
|
|
setIsVerifying(true);
|
|
onVerify(code.trim());
|
|
setTimeout(() => setIsVerifying(false), 2000); // Reset after delay
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900 mb-2">Or enter verification code manually:</p>
|
|
|
|
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={code}
|
|
onChange={(e) => setCode(e.target.value)}
|
|
placeholder="Enter 6-digit code"
|
|
maxLength={6}
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-center tracking-widest text-lg"
|
|
pattern="[0-9]{6}"
|
|
/>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={!code.trim() || isVerifying}
|
|
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium rounded-md transition-colors disabled:opacity-50"
|
|
>
|
|
{isVerifying ? 'Verifying...' : 'Verify'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
} |