/** * Standalone invite acceptance page (not wrapped in admin Shell). * Validates an invite token, then registers a passkey to complete signup. */ import { Input, Loader } from "@cloudflare/kumo"; import { Trans } from "@lingui/react/macro"; import { useLingui } from "@lingui/react/macro"; import { useSearch } from "@tanstack/react-router"; import * as React from "react"; import { validateInviteToken, type InviteVerifyResult } from "../lib/api"; import { PasskeyRegistration } from "./auth/PasskeyRegistration"; import { LogoLockup } from "./Logo.js"; import { RouterLinkButton } from "./RouterLinkButton.js"; type InviteStep = "verify" | "register" | "error"; interface RegisterStepProps { inviteData: InviteVerifyResult; token: string; } function handleInviteSuccess() { window.location.href = "/_emdash/admin"; } function RegisterStep({ inviteData, token }: RegisterStepProps) { const { t } = useLingui(); const [name, setName] = React.useState(""); return (

{t`You've been invited!`}

You'll be joining as{" "} {inviteData.roleName}

setName(e.target.value)} placeholder="Jane Doe" autoComplete="name" autoFocus />

{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; } function ErrorStep({ message, code }: ErrorStepProps) { const { t } = useLingui(); return (

{code === "TOKEN_EXPIRED" ? t`Invite expired` : code === "INVALID_TOKEN" ? t`Invalid invite link` : code === "USER_EXISTS" ? t`Account already exists` : t`Something went wrong`}

{message}

{code === "USER_EXISTS" ? ( {t`Sign in instead`} ) : ( <>

{t`Please ask your administrator to send a new invite.`}

{t`Back to login`} )}
); } export function InviteAcceptPage() { const { t } = useLingui(); const { token: urlToken } = useSearch({ strict: false }); const [step, setStep] = React.useState("verify"); const [error, setError] = React.useState(); const [errorCode, setErrorCode] = React.useState(); const [isLoading, setIsLoading] = React.useState(true); const [inviteData, setInviteData] = React.useState(null); const [token, setToken] = React.useState(null); React.useEffect(() => { if (!urlToken) { setError(t`No invite token provided`); setStep("error"); setIsLoading(false); return; } setToken(urlToken); void verifyToken(urlToken); }, [urlToken]); const verifyToken = async (tokenToVerify: string) => { setIsLoading(true); setError(undefined); setErrorCode(undefined); try { const result = await validateInviteToken(tokenToVerify); setInviteData(result); setStep("register"); } 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); } }; if (isLoading) { return (

{t`Verifying your invite...`}

); } return (

{step === "register" && t`Accept Invite`} {step === "error" && t`Invite Error`}

{step === "register" && inviteData && token && ( )} {step === "error" && ( )}
); } export default InviteAcceptPage;