project-standalo-sonic-cloud/app/components/EmailVerification.tsx

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>
);
}