import clsx from "clsx"; import * as React from "react"; import { Link, UNSAFE_FrameworkContext, useNavigate } from "react-router"; import { PasskeyLogo } from ".."; import { AUTH_HOST, browserSupportsWebAuthn, checkResponse, initRegistration, } from "../../utils/webauthn"; import { resolveRoute } from "../Link"; import * as styles from "./styles.module.css"; enum SignupState { RegisterUser, OTP, RegisterPasskey, Exists, } const titles: { [key in SignupState]: string } = { [SignupState.RegisterUser]: "Register", [SignupState.OTP]: "Confirm code", [SignupState.RegisterPasskey]: "Save a passkey", [SignupState.Exists]: "Already registered", }; const subtitles: { [key in SignupState]: (email: string | null) => string } = { [SignupState.RegisterUser]: (email) => "Enter your email to continue.", [SignupState.OTP]: (email) => `Please enter the code sent to "${email}".`, [SignupState.RegisterPasskey]: (email) => "Sign in to your account easily and securely with a passkey. Note: Your biometric data is only stored on your devices and never shared.", [SignupState.Exists]: (email) => `A user with email "${email}" as already been registered. Login?`, }; type Classes = { headline?: string; subtitle?: string; error?: string; container?: string; input?: string; primaryButton?: string; secondaryButton?: string; divider?: string; footer?: string; link?: string; }; export function Signup({ callbackUrl: _callbackUrl, loginUrl: _loginUrl, className, classes = {}, query = [], ...props }: { callbackUrl?: any; loginUrl?: any; query: { key: string; value: string }[]; className?: string; classes?: Classes; } & React.HTMLAttributes) { const [error, setError] = React.useState(null); const [step, setStep] = React.useState(SignupState.RegisterUser); const [email, setEmail] = React.useState(""); const framework = React.use(UNSAFE_FrameworkContext); if (!framework) { throw new Error( "Signup component must be used within a FrameworkContext provider.", ); } const callbackUrl = React.useMemo(() => { return resolveRoute(_callbackUrl, framework.manifest); }, [_callbackUrl, framework.manifest]); const loginUrl = React.useMemo(() => { return resolveRoute(_loginUrl, framework.manifest); }, [_loginUrl, framework.manifest]); const queryStr = React.useMemo( () => query?.length ? `?${new URLSearchParams(query.map(({ key, value }) => [key, value]))}` : "", [query], ); const renderStep = React.useCallback( (step: SignupState) => { switch (step) { case SignupState.RegisterUser: return ( ); case SignupState.OTP: return ( ); case SignupState.RegisterPasskey: return ( ); case SignupState.Exists: return ( ); default: // nothing to render for this route return null; } }, [email, callbackUrl, classes, queryStr], ); return (

{titles[step] ?? ""}

{subtitles[step]?.(email) ?? ""}

{error ? (
{error}
) : null} {renderStep(step)}
); } function RegisterUser({ setEmail, callbackUrl, loginUrl, setError, setStep, classes = {}, }: { setEmail: (email: string) => void; loginUrl?: string; callbackUrl?: string; setError: (error: string) => void; setStep: (step: SignupState) => void; classes?: Classes; }) { const navigate = useNavigate(); return ( <>
{ e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); setEmail(formData.get("email") as string); fetch(`${AUTH_HOST}/available`, { credentials: "include", method: "POST", body: formData, }) .then(checkResponse) .then((data) => { if (data.exists) { setStep(SignupState.Exists); } else { return fetch(`${AUTH_HOST}/magic-link/login`, { credentials: "include", method: "POST", body: formData, }) .then(checkResponse) .then((data) => { if (data.success) { setStep(SignupState.OTP); } else { throw new Error(data.error); } }) .catch((err) => { setError(`${err}`); }); } }) .catch((err) => { setError(`${err}`); }); }} > {callbackUrl ? ( ) : null}
{ navigate(loginUrl || "/"); }} > Login?
); } function ConfirmCode({ email, callbackUrl, setError, setStep, classes = {}, }: { email: string | null; callbackUrl?: string; setError: (error: string) => void; setStep: (step: SignupState) => void; classes?: Classes; }) { const navigate = useNavigate(); return (
{ e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); fetch(`${AUTH_HOST}/magic-link/verify`, { method: "POST", body: formData, credentials: "include", }) .then(checkResponse) .then((data) => { if (data.success) { // done! return fetch(`${AUTH_HOST}/me`, { credentials: "include", }); } else { throw new Error(data.error); } }) .then(checkResponse) .then((data) => { if ( !process.env.PREVIEW && browserSupportsWebAuthn() && shouldRegister(data?.authenticators) ) { setStep(SignupState.RegisterPasskey); } else { // done, redirect navigate(callbackUrl || "/"); } }) .catch((err) => { setError(`${err}`); }); }} >
setStep(SignupState.RegisterUser)} > Back
); } function shouldRegister(authenticators?: any[]) { if (!authenticators || !authenticators.length) { return true; } let saved: string[] = []; try { saved = JSON.parse(localStorage.getItem("__brevity_passkeys") || "[]"); } catch (err) { // ignore } if (saved.length === 0) { return true; } return !authenticators.some((authenticator) => { return saved.includes(authenticator?.id as string); }); } function RegisterPasskey({ callbackUrl, setError, setStep, classes = {}, }: { callbackUrl?: string; setError: (error: string) => void; setStep: (step: SignupState) => void; classes?: Classes; }) { const navigate = useNavigate(); const [supportsWebauthn, setSupportsWebauthn] = React.useState(true); React.useEffect(() => { setSupportsWebauthn(browserSupportsWebAuthn()); }, []); return (
What are passkeys? Skip
); } function GoToLogin({ setStep, loginUrl, classes = {}, }: { setStep: (step: SignupState) => void; loginUrl?: string; classes?: Classes; }) { const navigate = useNavigate(); return (
setStep(SignupState.RegisterUser)} > Back
); }