MotorSignal-App/website/components/verify-email-form.tsx

251 lines
8.8 KiB
TypeScript

'use client';
import { FormEvent, useEffect, useState } from 'react';
import Link from 'next/link';
import { SegmentedOtpInput } from './segmented-otp-input';
export function VerifyEmailForm() {
const [token, setToken] = useState('');
const [email, setEmail] = useState('');
const [showEmailInput, setShowEmailInput] = useState(false);
const [message, setMessage] = useState('');
const [isSuccess, setIsSuccess] = useState(false);
const [busy, setBusy] = useState(false);
const [resendCooldown, setResendCooldown] = useState(60);
const [resendBusy, setResendBusy] = useState(false);
const [isLongToken, setIsLongToken] = useState(false);
useEffect(() => {
const searchToken = new URLSearchParams(window.location.search).get('token') ?? '';
if (searchToken) {
setToken(searchToken);
if (searchToken.length !== 6) {
setIsLongToken(true);
}
}
}, []);
// Cooldown countdown timer
useEffect(() => {
if (resendCooldown <= 0) return;
const timer = setInterval(() => {
setResendCooldown((prev) => Math.max(0, prev - 1));
}, 1000);
return () => clearInterval(timer);
}, [resendCooldown]);
async function submit(event?: FormEvent) {
if (event) event.preventDefault();
if (!token.trim()) return;
setBusy(true);
setMessage('');
setIsSuccess(false);
try {
const response = await fetch('/api/auth/verify-email', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ token: token.trim() }),
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body.detail ?? 'Unable to verify email signature.');
}
setIsSuccess(true);
setMessage('Telemetry signature verified successfully. Your account is fully active.');
} catch (error) {
setIsSuccess(false);
setMessage(error instanceof Error ? error.message : 'Unable to verify email.');
} finally {
setBusy(false);
}
}
async function handleResendCode() {
if (!email.trim() && !showEmailInput) {
setShowEmailInput(true);
return;
}
if (!email.trim()) {
setMessage('Please enter your registered email address to receive a new code.');
return;
}
setResendBusy(true);
setMessage('');
try {
const response = await fetch('/api/auth/verify-email-request', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: email.trim() }),
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body.detail ?? 'Unable to resend verification code.');
}
setResendCooldown(60);
setShowEmailInput(false);
setMessage('Fresh verification code dispatched to your registered address.');
} catch (error) {
setMessage(error instanceof Error ? error.message : 'Unable to resend verification code.');
} finally {
setResendBusy(false);
}
}
return (
<div className="space-y-6">
{/* Tactical Header Badge */}
<div className="flex flex-col items-center text-center space-y-2">
<div className="w-14 h-14 rounded-2xl bg-[#00f2fe]/10 border border-[#00f2fe]/30 flex items-center justify-center text-[#00f2fe] shadow-[0_0_20px_rgba(0,242,254,0.25)]">
<svg
className="w-7 h-7"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
</div>
<h2 className="text-xl sm:text-2xl font-black tracking-wide text-white font-mono uppercase">
Verify Telemetry Signature
</h2>
<p className="text-xs sm:text-sm text-[#b9cacb] max-w-sm">
Enter the 6-digit cryptographic authorization code dispatched to your registered address.
</p>
</div>
{isLongToken && (
<div className="p-3 bg-[#00f2fe]/10 border border-[#00f2fe]/30 rounded-xl text-xs font-mono text-[#00dce6] flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-[#00f2fe] animate-pulse"></span>
<span>Cryptographic token detected from authentication link.</span>
</div>
)}
{/* Main Challenge Form */}
<form onSubmit={submit} className="space-y-6">
{isLongToken ? (
<div>
<label className="block text-xs font-mono text-[#b9cacb] uppercase tracking-wider mb-2">
Cryptographic Token
</label>
<input
type="text"
value={token}
onChange={(e) => setToken(e.target.value)}
required
className="w-full px-4 py-3 bg-[#070a0f] border border-[#262a33] rounded-xl text-white font-mono text-sm focus:border-[#00f2fe] outline-none"
/>
</div>
) : (
<div className="py-2">
<SegmentedOtpInput
value={token}
onChange={(val) => {
setToken(val);
setMessage('');
}}
disabled={busy || isSuccess}
/>
</div>
)}
{/* Primary Action Button */}
<button
type="submit"
disabled={busy || isSuccess || (!isLongToken && token.length < 6)}
className="w-full py-3.5 px-6 rounded-xl font-mono text-sm font-bold tracking-wider uppercase transition-all duration-200
bg-[#00f2fe] text-[#002022] hover:bg-[#6ff6ff] hover:shadow-[0_0_24px_rgba(0,242,254,0.4)]
disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{busy ? (
<>
<span className="w-4 h-4 border-2 border-[#002022] border-t-transparent rounded-full animate-spin"></span>
<span>Validating Signature...</span>
</>
) : isSuccess ? (
<>
<span>✓ Signature Confirmed</span>
</>
) : (
'Confirm Telemetry Signature'
)}
</button>
{/* Status Message */}
{message && (
<div
role="status"
className={`p-3.5 rounded-xl text-xs sm:text-sm font-mono border ${
isSuccess
? 'bg-[#60ff98]/10 border-[#60ff98]/40 text-[#60ff98]'
: 'bg-[#ffb4ab]/10 border-[#ffb4ab]/40 text-[#ffb4ab]'
}`}
>
{message}
</div>
)}
</form>
{/* Resend Code Section */}
<div className="pt-4 border-t border-[#262a33] text-center space-y-3">
{showEmailInput && (
<div className="space-y-2 text-left">
<label className="block text-xs font-mono text-[#b9cacb]">
Registered Email Address
</label>
<div className="flex gap-2">
<input
type="email"
placeholder="rider@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="flex-1 px-3 py-2 bg-[#070a0f] border border-[#262a33] rounded-lg text-white text-xs font-mono focus:border-[#00f2fe] outline-none"
/>
<button
type="button"
onClick={handleResendCode}
disabled={resendBusy}
className="px-4 py-2 bg-[#181c24] hover:bg-[#262a33] border border-[#3a494b] text-white rounded-lg text-xs font-mono font-bold"
>
{resendBusy ? 'Sending...' : 'Send'}
</button>
</div>
</div>
)}
<div className="flex items-center justify-between text-xs font-mono text-[#b9cacb]">
<span>Didn't receive the code?</span>
{resendCooldown > 0 ? (
<span className="text-[#849495]">Resend code in {resendCooldown}s</span>
) : (
<button
type="button"
onClick={handleResendCode}
disabled={resendBusy}
className="text-[#00dce6] hover:text-[#6ff6ff] font-semibold underline underline-offset-4"
>
{resendBusy ? 'Dispatching...' : 'Resend Authorization Code'}
</button>
)}
</div>
<div className="pt-2">
<Link
href="/login"
className="inline-block text-xs font-mono text-[#849495] hover:text-white transition-colors"
>
← Return to sign in
</Link>
</div>
</div>
</div>
);
}