/** * Signup Page - Self-signup for allowed domains * * This component is NOT wrapped in the admin Shell. * It's a standalone public page for self-signup. * * Flow: * 1. Email input form * 2. "Check your email" confirmation * 3. After clicking email link: Passkey registration */ import { Button, Input, Loader } from "@cloudflare/kumo"; import { useLingui } from "@lingui/react/macro"; import { Link } from "@tanstack/react-router"; import * as React from "react"; import { requestSignup, verifySignupToken, type SignupVerifyResult } from "../lib/api"; import { PasskeyRegistration } from "./auth/PasskeyRegistration"; import { LogoLockup } from "./Logo.js"; // ============================================================================ // Types // ============================================================================ type SignupStep = "email" | "check-email" | "verify" | "complete" | "error"; // ============================================================================ // Step Components // ============================================================================ interface EmailStepProps { onSubmit: (email: string) => void; isLoading: boolean; error?: string; } function EmailStep({ onSubmit, isLoading, error }: EmailStepProps) { const { t } = useLingui(); const [email, setEmail] = React.useState(""); const [validationError, setValidationError] = React.useState(null); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setValidationError(null); if (!email.trim()) { setValidationError(t`Email is required`); return; } if (!email.includes("@") || !email.includes(".")) { setValidationError(t`Please enter a valid email address`); return; } onSubmit(email.trim().toLowerCase()); }; return (
setEmail(e.target.value)} placeholder={t`you@company.com`} className={validationError ? "border-kumo-danger" : ""} disabled={isLoading} autoComplete="email" autoFocus /> {validationError &&

{validationError}

}
{error && (
{error}
)}

{t`Only email addresses from allowed domains can sign up.`}

); } interface CheckEmailStepProps { email: string; onResend: () => void; isResending: boolean; resendCooldown: number; } function CheckEmailStep({ email, onResend, isResending, resendCooldown }: CheckEmailStepProps) { const { t } = useLingui(); return (

{t`Check your email`}

{t`We've sent a verification link to`}{" "} {email}

{t`Click the link in the email to continue setting up your account.`}

{t`The link will expire in 15 minutes.`}

{t`Didn't receive the email?`}

); } interface VerifyStepProps { verifyResult: SignupVerifyResult; token: string; onBack: () => void; } function handleSignupSuccess() { // Redirect to admin dashboard after successful signup window.location.href = "/_emdash/admin"; } function VerifyStep({ verifyResult, token, onBack: _onBack }: VerifyStepProps) { const { t } = useLingui(); const [name, setName] = React.useState(""); return (

{t`Email verified!`}

{t`You'll be signing up as`}{" "} {verifyResult.roleName}

{/* Email display (read-only) */} {/* Name input (optional) */} setName(e.target.value)} placeholder={t`Jane Doe`} autoComplete="name" /> {/* Passkey registration */}

{t`Create your passkey`}

{t`Passkeys are a secure, passwordless way to sign in using your device's biometrics, PIN, or security key.`}

); } interface ErrorStepProps { message: string; code?: string; onRetry?: () => void; } function ErrorStep({ message, code, onRetry }: ErrorStepProps) { const { t } = useLingui(); return (

{code === "token_expired" ? t`Link expired` : code === "invalid_token" ? t`Invalid link` : code === "user_exists" ? t`Account exists` : t`Something went wrong`}

{message}

{code === "user_exists" ? ( ) : ( onRetry && ( ) )}
); } // ============================================================================ // Main Component // ============================================================================ export function SignupPage() { const [step, setStep] = React.useState("email"); const [email, setEmail] = React.useState(""); const [error, setError] = React.useState(); const [errorCode, setErrorCode] = React.useState(); const [isLoading, setIsLoading] = React.useState(false); const [verifyResult, setVerifyResult] = React.useState(null); const [token, setToken] = React.useState(null); const [resendCooldown, setResendCooldown] = React.useState(0); // Check for token in URL on mount React.useEffect(() => { const params = new URLSearchParams(window.location.search); const urlToken = params.get("token"); if (urlToken) { setToken(urlToken); void verifyToken(urlToken); } }, []); // Resend cooldown timer React.useEffect(() => { if (resendCooldown > 0) { const timer = setTimeout(() => setResendCooldown((c) => c - 1), 1000); return () => clearTimeout(timer); } }, [resendCooldown]); const verifyToken = async (tokenToVerify: string) => { setIsLoading(true); setError(undefined); setErrorCode(undefined); try { const result = await verifySignupToken(tokenToVerify); setVerifyResult(result); setStep("verify"); } catch (err) { const verifyError = err instanceof Error ? err : new Error(String(err)); const errorWithCode = verifyError as Error & { code?: string }; setError(verifyError.message); setErrorCode(typeof errorWithCode.code === "string" ? errorWithCode.code : undefined); setStep("error"); } finally { setIsLoading(false); } }; const handleEmailSubmit = async (submittedEmail: string) => { setIsLoading(true); setError(undefined); setEmail(submittedEmail); try { await requestSignup(submittedEmail); setStep("check-email"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to send verification email"); } finally { setIsLoading(false); } }; const handleResend = async () => { if (!email || resendCooldown > 0) return; setIsLoading(true); try { await requestSignup(email); setResendCooldown(60); // 60 second cooldown } catch { // Silently fail - don't reveal if email exists } finally { setIsLoading(false); } }; const handleRetry = () => { setStep("email"); setError(undefined); setErrorCode(undefined); setToken(null); // Clear token from URL window.history.replaceState({}, "", window.location.pathname); }; const { t } = useLingui(); // Loading state for token verification if (isLoading && token) { return (

{t`Verifying your link...`}

); } return (
{/* Header */}

{step === "email" && t`Create an account`} {step === "check-email" && t`Check your email`} {step === "verify" && t`Complete signup`} {step === "error" && t`Oops!`}

{/* Form Card */}
{step === "email" && ( )} {step === "check-email" && ( )} {step === "verify" && verifyResult && token && ( )} {step === "error" && ( )}
{/* Login link */} {step === "email" && (

{t`Already have an account?`}{" "} {t`Sign in`}

)}
); } export default SignupPage;