251 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|