import { WebAuthnAbortService } from "@simplewebauthn/browser"; import clsx from "clsx"; import { OTPInput, OTPInputContext, OTPInputProps } from "input-otp"; import * as React from "react"; import { Link, UNSAFE_FrameworkContext, useNavigate, useSearchParams, } from "react-router"; import { AUTH_HOST, browserSupportsWebAuthn, checkResponse, initLogin, initRegistration, } from "../../utils/webauthn"; import { resolveRoute } from "../Link"; import * as styles from "./styles.module.css"; enum LoginState { Login, Passkey, RegisterUser, OTP, RegisterPasskey, NotFound, } const titles: { [key in LoginState]: string } = { [LoginState.Login]: "Welcome", [LoginState.Passkey]: "Sign in with passkey?", [LoginState.RegisterUser]: "Create account?", [LoginState.OTP]: "Confirm code", [LoginState.RegisterPasskey]: "Sign in faster with a passkey", [LoginState.NotFound]: "Account not found", }; const subtitles: { [key in LoginState]: (email: string | null) => string } = { [LoginState.Login]: (email) => "Enter your email to continue.", [LoginState.Passkey]: (email) => "Use a passkey to sign in or try verification code instead.", [LoginState.RegisterUser]: (email) => `No account exists for email "${email}". Would you like to create one?`, [LoginState.OTP]: (email) => `Please enter the code sent to "${email}".`, [LoginState.RegisterPasskey]: (email) => "Use your fingerprint, face, or pin to sign in.", [LoginState.NotFound]: (email) => `No account exists for email "${email}".`, }; type Classes = { headline?: string; subtitle?: string; error?: string; container?: string; input?: string; otp?: string; primaryButton?: string; secondaryButton?: string; divider?: string; footer?: string; link?: string; }; export function Login({ title = "Welcome", subtitle = "Enter your email to continue.", callbackUrl: _callbackUrl, className, disablePasskey, disableCreate, classes = {}, query = [], ...props }: { title?: string; subtitle?: string; callbackUrl?: any; disablePasskey?: boolean; query: { key: string; value: string }[]; className?: string; disableCreate?: boolean; classes?: Classes; } & React.HTMLAttributes) { const [error, setError] = React.useState(null); const [step, setStep] = React.useState(LoginState.Login); const [email, setEmail] = React.useState(""); const framework = React.use(UNSAFE_FrameworkContext); if (!framework) { throw new Error( "Framework context is not available. Please ensure you are using this component within a FrameworkProvider.", ); } const callbackUrl = React.useMemo(() => { return resolveRoute(_callbackUrl, framework.manifest); }, [_callbackUrl, framework.manifest]); const queryStr = React.useMemo( () => query?.length ? `?${new URLSearchParams(query.map(({ key, value }) => [key, value]))}` : "", [query], ); const renderStep = React.useCallback( (step: LoginState) => { switch (step) { case LoginState.Login: return ( ); case LoginState.Passkey: return ( ); case LoginState.RegisterUser: return ( ); case LoginState.OTP: return ( ); case LoginState.RegisterPasskey: return ( ); case LoginState.NotFound: // nothing to render for this route return null; } }, [email, disableCreate, callbackUrl, classes, queryStr], ); return (

{(step === LoginState.Login ? title : titles[step]) ?? ""}

{(step === LoginState.Login ? subtitle : subtitles[step]?.(email)) ?? ""}

{error ? (
{error}
) : null} {renderStep(step)}
); } function Initial({ setEmail, callbackUrl, setError, setStep, disableCreate, disablePasskey, classes = {}, ...props }: { setEmail: (email: string) => void; callbackUrl?: string; setError: (error: string | null) => void; setStep: (step: LoginState) => void; disableCreate?: boolean; disablePasskey?: boolean; classes?: Classes; }) { const [enableGoogle, setEnableGoogle] = React.useState(false); const [enableEntra, setEnableEntra] = React.useState(false); const [supportsWebauthn, setSupportsWebauthn] = React.useState(true); const [searchParams] = useSearchParams(); const navigate = useNavigate(); React.useEffect(() => { if (process.env.PREVIEW) { setSupportsWebauthn(browserSupportsWebAuthn()); return; } if (disablePasskey) { return; } const signal = WebAuthnAbortService.createNewAbortSignal(); initLogin(true, signal) .then(({ verified, credentialID }) => { if (verified) { if (credentialID) { // append to list in local storage try { const list = JSON.parse( localStorage.getItem("__brevity_passkeys") || "[]", ); list.push(credentialID); localStorage.setItem("__brevity_passkeys", JSON.stringify(list)); } catch (err) { // ignore } } navigate(callbackUrl || "/"); } else { // do nothing, wait for user to click button // setError("Failed to sign in with passkey"); } }) .catch((err) => { if (!err) return; if (err.name === "AbortError" || err.name === "NotAllowedError") { // This error is thrown when the user cancels the operation in the browser return; } if (err?.code) { if (err.code === "ERROR_CEREMONY_ABORTED") { // This error is thrown when the user cancels the operation in the browser return; } else if (err.code === "ERROR_INVALID_DOMAIN") { setError(`Invalid domain`); return; } else if (err.code === "ERROR_INVALID_RP_ID") { setError(`Invalid RP ID`); return; } else if (err.code === "ERROR_INVALID_USER_ID_LENGTH") { setError(`Invalid user ID length`); return; } else if (err.code === "ERROR_MALFORMED_PUBKEYCREDPARAMS") { setError(`Malformed public key credential parameters`); return; } else if (err.code === "ERROR_AUTHENTICATOR_GENERAL_ERROR") { setError(`Authenticator general error`); return; } else if ( err.code === "ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT" ) { setError(`Authenticator missing discoverable credential support`); return; } else if ( err.code === "ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT" ) { setError(`Authenticator missing user verification support`); return; } else if (err.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED") { setError(`Authenticator previously registered`); return; } else if ( err.code === "ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG" ) { setError( `Authenticator no supported public key credential parameters algorithm`, ); return; } else if (err.code === "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY") { err = err.cause || err; } } if ( err.message === "Browser does not support WebAuthn autofill" || err.message === "WebAuthn is not supported in this browser" ) { // This errors match simplewebauthn's error messages console.error(err); return; } console.error(err); setError(`${err}`); }); setSupportsWebauthn(browserSupportsWebAuthn()); return () => { WebAuthnAbortService.cancelCeremony(); }; }, []); React.useEffect(() => { const abortController = new AbortController(); fetch(`${AUTH_HOST}/oauth`, { signal: abortController.signal, }) .then(checkResponse) .then((data: Record) => { if (data.google) { setEnableGoogle(true); } if (data.entra) { setEnableEntra(true); } }); return () => { abortController.abort(); }; }, []); return (
{ e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); setEmail(formData.get("identifier") as string); fetch(`${AUTH_HOST}/available`, { credentials: "include", method: "POST", body: formData, }) .then(checkResponse) .then((data) => { if (data.exists) { if (data.hasPasskey && supportsWebauthn && !disablePasskey) { setError(null); setStep(LoginState.Passkey); return; } return fetch(`${AUTH_HOST}/magic-link/login`, { credentials: "include", method: "POST", body: formData, }) .then(checkResponse) .then((data) => { if (data.success) { setError(null); setStep(LoginState.OTP); } else { setError(data.error); } }); } else if (disableCreate) { setError(null); setStep(LoginState.NotFound); } else { setError(null); setStep(LoginState.RegisterUser); } }) .catch((err) => { setError(`${err}`); }); }} > {callbackUrl ? ( ) : null} {disableCreate ? ( ) : null} {enableGoogle || enableEntra ? (
or continue with
) : null} {enableGoogle ? ( Google ) : null} {enableEntra ? ( Microsoft Entra ID ) : null}
); } function Passkey({ email, callbackUrl, setError, setStep, disableCreate, classes = {}, ...props }: { email: string; callbackUrl?: string; setError: (error: string | null) => void; setStep: (step: LoginState) => void; disableCreate?: boolean; classes?: Classes; }) { const [supportsWebauthn, setSupportsWebauthn] = React.useState(true); const [searchParams] = useSearchParams(); const navigate = useNavigate(); React.useEffect(() => { const hasSupport = browserSupportsWebAuthn(); setSupportsWebauthn(hasSupport); }, []); const startPasskey = React.useCallback(() => { if (process.env.PREVIEW) { return; } if (!supportsWebauthn) { return; } const signal = WebAuthnAbortService.createNewAbortSignal(); initLogin(false, signal) .then(({ verified, credentialID }) => { if (verified) { if (credentialID) { // append to list in local storage try { const list = JSON.parse( localStorage.getItem("__brevity_passkeys") || "[]", ); list.push(credentialID); localStorage.setItem("__brevity_passkeys", JSON.stringify(list)); } catch (err) { // ignore } } navigate(callbackUrl || "/"); } else { // do nothing, wait for user to click button setError( "Failed to sign in with passkey. Try verification code instead.", ); } }) .catch((err) => { if (!err) return; if (err?.name === "AbortError" || err?.name === "NotAllowedError") { // This error is thrown when the user cancels the operation in the browser return; } if (err?.code) { if (err.code === "ERROR_CEREMONY_ABORTED") { // This error is thrown when the user cancels the operation in the browser return; } else if (err.code === "ERROR_INVALID_DOMAIN") { setError(`Invalid domain`); return; } else if (err.code === "ERROR_INVALID_RP_ID") { setError(`Invalid RP ID`); return; } else if (err.code === "ERROR_INVALID_USER_ID_LENGTH") { setError(`Invalid user ID length`); return; } else if (err.code === "ERROR_MALFORMED_PUBKEYCREDPARAMS") { setError(`Malformed public key credential parameters`); return; } else if (err.code === "ERROR_AUTHENTICATOR_GENERAL_ERROR") { setError(`Authenticator general error`); return; } else if ( err.code === "ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT" ) { setError(`Authenticator missing discoverable credential support`); return; } else if ( err.code === "ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT" ) { setError(`Authenticator missing user verification support`); return; } else if (err.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED") { setError(`Authenticator previously registered`); return; } else if ( err.code === "ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG" ) { setError( `Authenticator no supported public key credential parameters algorithm`, ); return; } else if (err.code === "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY") { err = err.cause || err; } } if ( err.message === "Browser does not support WebAuthn autofill" || err.message === "WebAuthn is not supported in this browser" ) { // This errors match simplewebauthn's error messages console.error(err); return; } console.error(err); setError(`${err}`); }); return () => { WebAuthnAbortService.cancelCeremony(); }; }, []); return (
{ e.preventDefault(); if (process.env.PREVIEW) { navigate(callbackUrl || "/"); return; } const formData = new FormData(); formData.set("identifier", email ?? ""); formData.set("callbackUrl", callbackUrl || "/"); return fetch(`${AUTH_HOST}/magic-link/login`, { credentials: "include", method: "POST", body: formData, }) .then(checkResponse) .then((data) => { if (data.success) { setError(null); setStep(LoginState.OTP); } else { setError(data.error); } }); }} > {supportsWebauthn ? ( ) : null}
{ setError(null); setStep(LoginState.Login); }} > Back
); } export function PasskeyLogo() { return ( ); } function ConfirmRegistration({ callbackUrl, setStep, email, setError, classes = {}, }: { callbackUrl?: string; setStep: (step: LoginState) => void; email: string | null; setError: (error: string | null) => void; classes?: Classes; }) { return (
{ e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); fetch(`${AUTH_HOST}/magic-link/login`, { credentials: "include", method: "POST", body: formData, }) .then(checkResponse) .then((data) => { if (data.success) { setError(null); setStep(LoginState.OTP); } else { throw new Error(data.error); } }) .catch((err) => { setError(`${err}`); }); }} >
{ setError(null); setStep(LoginState.Login); }} > Back
); } function ConfirmCode({ email, callbackUrl, setError, setStep, disablePasskey, classes = {}, }: { email: string | null; callbackUrl?: string; setError: (error: string | null) => void; setStep: (step: LoginState) => void; disablePasskey?: boolean; classes?: Classes; }) { const navigate = useNavigate(); return ( <> { const formData = new FormData(); formData.set("identifier", email ?? ""); formData.set("token", token); formData.set("callbackUrl", callbackUrl || "/"); 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) && !disablePasskey ) { setError(null); setStep(LoginState.RegisterPasskey); } else { // done, redirect navigate(callbackUrl || "/"); } }) .catch((err) => { setError(`${err}`); }); }} >
{ setError(null); setStep(LoginState.Login); }} > Back
); } export function OTP({ className, ...props }: OTPInputProps) { return ( ); } OTP.OTPSlot = OTPSlot; function OTPSlot({ index, className, ...props }) { const inputOTPContext = React.useContext(OTPInputContext); const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]; return (
{char} {hasFakeCaret && }
); } // Emulate a fake textbox caret! function FakeCaret() { return (
); } 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 | null) => void; setStep: (step: LoginState) => void; classes?: Classes; }) { const navigate = useNavigate(); const [supportsWebauthn, setSupportsWebauthn] = React.useState(true); React.useEffect(() => { setSupportsWebauthn(browserSupportsWebAuthn()); }, []); // React.useEffect(() => { // const ac = new AbortController(); // if (!browserSupportsWebAuthnAutofill()) { // return; // } // if (process.env.PREVIEW) { // navigate(callbackUrl || "/"); // return; // } // initRegistration(ac.signal, true) // .then(({ verified, credentialID }) => { // if (verified) { // if (credentialID) { // // append to list in local storage // try { // const list = JSON.parse( // localStorage.getItem("__brevity_passkeys") || "[]", // ); // list.push(credentialID); // localStorage.setItem("__brevity_passkeys", JSON.stringify(list)); // } catch (err) { // // ignore // } // } // navigate(callbackUrl || "/"); // } // }) // .catch((err) => { // if (!err) return; // if (err.name === "AbortError" || err.name === "NotAllowedError") { // // This error is thrown when the user cancels the operation in the browser // return; // } else if (err.name === "InvalidStateError") { // navigate(callbackUrl || "/"); // } else { // console.error(err); // setError(`${err}`); // } // }); // return () => ac.abort(); // }, []); return (
Maybe later
Learn more about passkeys
); }