Skip to content

Login Page

A production-ready login page with every auth method. Copy this into your project and customize.

import { createSnapshot, isMfaChallenge } from "@lastshotlabs/snapshot";
import {
InputField,
ButtonBase,
CardBase,
OAuthButtonsBase,
PasskeyButtonBase,
RowBase,
ColumnBase,
AlertBase,
} from "@lastshotlabs/snapshot/ui";
import { useState, useEffect } from "react";
const snap = createSnapshot({
apiUrl: "/api",
loginPath: "/login",
homePath: "/",
auth: {
providers: {
google: {
type: "google",
clientId: "YOUR_GOOGLE_CLIENT_ID",
scopes: ["email", "profile"],
callbackPath: "/auth/callback",
},
github: {
type: "github",
clientId: "YOUR_GITHUB_CLIENT_ID",
scopes: ["user:email"],
callbackPath: "/auth/callback",
},
},
},
});
export function LoginPage() {
const {
mutate: login,
isPending,
error: loginError,
reset,
} = snap.useLogin();
const {
mutate: verify,
isPending: verifying,
error: verifyError,
} = snap.useMfaVerify();
const challenge = snap.usePendingMfaChallenge();
const { mutate: getPasskeyOptions } = snap.usePasskeyLoginOptions();
const { mutate: passkeyLogin } = snap.usePasskeyLogin();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [mfaCode, setMfaCode] = useState("");
const [passkeyError, setPasskeyError] = useState<string | null>(null);
const [view, setView] = useState<"login" | "forgot" | "register">("login");
// ── MFA verification ──────────────────────────────────────────────────
if (challenge) {
return (
<CenteredCard title="Two-factor authentication">
<p style={{ color: "var(--sn-color-muted-foreground)" }}>
Enter the code from your{" "}
{challenge.mfaMethods[0] === "totp" ? "authenticator app" : "email"}.
</p>
<form
onSubmit={(e) => {
e.preventDefault();
verify({ code: mfaCode, method: challenge.mfaMethods[0] });
}}
>
<ColumnBase gap="md">
<InputField
label="Verification code"
value={mfaCode}
onChange={setMfaCode}
placeholder="000000"
maxLength={6}
/>
{verifyError && (
<AlertBase severity="error">
{snap.formatAuthError(verifyError)}
</AlertBase>
)}
<ButtonBase
label={verifying ? "Verifying..." : "Verify"}
type="submit"
disabled={verifying || !mfaCode}
fullWidth
/>
</ColumnBase>
</form>
</CenteredCard>
);
}
// ── Forgot password ───────────────────────────────────────────────────
if (view === "forgot") {
return <ForgotPasswordView onBack={() => setView("login")} />;
}
// ── Registration ──────────────────────────────────────────────────────
if (view === "register") {
return <RegisterView onBack={() => setView("login")} />;
}
// ── Login ─────────────────────────────────────────────────────────────
return (
<CenteredCard title="Sign in">
<form
onSubmit={(e) => {
e.preventDefault();
reset(); // clear previous errors
setPasskeyError(null);
login({ email, password });
}}
>
<ColumnBase gap="md">
<InputField
label="Email"
type="email"
value={email}
onChange={setEmail}
required
/>
<InputField
label="Password"
type="password"
value={password}
onChange={setPassword}
required
/>
{loginError && (
<AlertBase severity="error">
{snap.formatAuthError(loginError)}
</AlertBase>
)}
<ButtonBase
label={isPending ? "Signing in..." : "Sign in"}
type="submit"
disabled={isPending}
fullWidth
/>
</ColumnBase>
</form>
<Divider />
<OAuthButtonsBase
providers={[
{
name: "google",
label: "Continue with Google",
url: snap.getOAuthUrl("google"),
},
{
name: "github",
label: "Continue with GitHub",
url: snap.getOAuthUrl("github"),
},
]}
onProviderClick={(url) => {
window.location.href = url;
}}
/>
<PasskeyButtonBase
label="Sign in with passkey"
onClick={() => {
setPasskeyError(null);
getPasskeyOptions(
{},
{
onSuccess: async (response) => {
try {
const assertion = await navigator.credentials.get({
publicKey: response.options as any,
});
if (assertion) {
const rawId = new Uint8Array((assertion as any).rawId);
const authData = new Uint8Array(
(assertion as any).response.authenticatorData,
);
const clientData = new Uint8Array(
(assertion as any).response.clientDataJSON,
);
const sig = new Uint8Array(
(assertion as any).response.signature,
);
passkeyLogin({
id: assertion.id,
rawId: btoa(String.fromCharCode(...rawId)),
response: {
authenticatorData: btoa(
String.fromCharCode(...authData),
),
clientDataJSON: btoa(
String.fromCharCode(...clientData),
),
signature: btoa(String.fromCharCode(...sig)),
},
});
}
} catch (err) {
if ((err as Error).name !== "NotAllowedError") {
setPasskeyError(
"Passkey authentication failed. Try another method.",
);
}
// NotAllowedError = user cancelled the prompt, not an error
}
},
onError: () => {
setPasskeyError(
"Could not start passkey login. Your browser may not support it.",
);
},
},
);
}}
/>
{passkeyError && <AlertBase severity="error">{passkeyError}</AlertBase>}
<RowBase justify="between">
<ButtonBase
label="Forgot password?"
variant="link"
onClick={() => setView("forgot")}
/>
<ButtonBase
label="Create account"
variant="link"
onClick={() => setView("register")}
/>
</RowBase>
</CenteredCard>
);
}
// ── Registration Form ─────────────────────────────────────────────────────
function RegisterView({ onBack }: { onBack: () => void }) {
const { mutate: register, isPending, error } = snap.useRegister();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const passwordMismatch = confirm.length > 0 && confirm !== password;
return (
<CenteredCard title="Create account">
<form
onSubmit={(e) => {
e.preventDefault();
if (passwordMismatch) return;
register({ name, email, password });
}}
>
<ColumnBase gap="md">
<InputField
label="Full name"
value={name}
onChange={setName}
required
/>
<InputField
label="Email"
type="email"
value={email}
onChange={setEmail}
required
/>
<InputField
label="Password"
type="password"
value={password}
onChange={setPassword}
required
/>
<InputField
label="Confirm password"
type="password"
value={confirm}
onChange={setConfirm}
required
errorText={passwordMismatch ? "Passwords don't match" : undefined}
/>
{error && (
<AlertBase severity="error">
{snap.formatAuthError(error)}
</AlertBase>
)}
<ButtonBase
label={isPending ? "Creating account..." : "Create account"}
type="submit"
disabled={isPending || passwordMismatch}
fullWidth
/>
</ColumnBase>
</form>
<ButtonBase
label="Already have an account? Sign in"
variant="link"
onClick={onBack}
/>
</CenteredCard>
);
}
// ── Forgot Password ───────────────────────────────────────────────────────
function ForgotPasswordView({ onBack }: { onBack: () => void }) {
const {
mutate: forgot,
isSuccess,
isPending,
error,
} = snap.useForgotPassword();
const [email, setEmail] = useState("");
if (isSuccess) {
return (
<CenteredCard title="Check your email">
<p>
We sent a password reset link to <strong>{email}</strong>. It expires
in 1 hour.
</p>
<ButtonBase
label="Back to sign in"
variant="outline"
onClick={onBack}
fullWidth
/>
</CenteredCard>
);
}
return (
<CenteredCard title="Reset password">
<p style={{ color: "var(--sn-color-muted-foreground)" }}>
Enter your email and we'll send a reset link.
</p>
<form
onSubmit={(e) => {
e.preventDefault();
forgot({ email });
}}
>
<ColumnBase gap="md">
<InputField
label="Email"
type="email"
value={email}
onChange={setEmail}
required
/>
{error && (
<AlertBase severity="error">
{snap.formatAuthError(error)}
</AlertBase>
)}
<ButtonBase
label={isPending ? "Sending..." : "Send reset link"}
type="submit"
disabled={isPending || !email}
fullWidth
/>
</ColumnBase>
</form>
<ButtonBase label="Back to sign in" variant="link" onClick={onBack} />
</CenteredCard>
);
}
// ── OAuth Callback Page ───────────────────────────────────────────────────
/**
* Mount this component at your redirectUri (e.g. /auth/callback).
* It reads the `code` and `provider` params, exchanges them for a session,
* and redirects to homePath on success.
*/
export function OAuthCallbackPage() {
const { mutate: exchange, error, isPending } = snap.useOAuthExchange();
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const provider = params.get("provider") ?? params.get("state") ?? "google";
useEffect(() => {
if (code) exchange({ code, provider });
}, []);
if (error) {
return (
<CenteredCard title="Sign-in failed">
<AlertBase severity="error">{snap.formatAuthError(error)}</AlertBase>
<ButtonBase
label="Try again"
onClick={() => (window.location.href = "/login")}
fullWidth
/>
</CenteredCard>
);
}
return (
<CenteredCard title="Signing you in...">
<p style={{ color: "var(--sn-color-muted-foreground)" }}>
{isPending ? "Completing sign-in..." : "Redirecting..."}
</p>
</CenteredCard>
);
}
// ── Shared Components ─────────────────────────────────────────────────────
function CenteredCard({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
padding: "1rem",
}}
>
<CardBase
title={title}
style={{ width: "100%", maxWidth: "420px" }}
gap="lg"
>
{children}
</CardBase>
</div>
);
}
function Divider() {
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: "1rem",
margin: "0.5rem 0",
}}
>
<hr
style={{
flex: 1,
border: "none",
borderTop: "1px solid var(--sn-color-border)",
}}
/>
<span
style={{
color: "var(--sn-color-muted-foreground)",
fontSize: "0.875rem",
}}
>
or
</span>
<hr
style={{
flex: 1,
border: "none",
borderTop: "1px solid var(--sn-color-border)",
}}
/>
</div>
);
}
  • Email/password login with formatAuthError error display
  • MFA verification with TOTP and email OTP support
  • Google and GitHub OAuth with proper OAuthProvider objects
  • Passkey/WebAuthn login with try-catch for browser errors
  • Registration form with password confirmation
  • Forgot password flow with success confirmation
  • OAuth callback page that exchanges the auth code for a session

Mount OAuthCallbackPage at whatever path you set for the provider callbackPath. With TanStack Router:

routes/auth/callback.tsx
import { createFileRoute } from "@tanstack/react-router";
import { OAuthCallbackPage } from "./login";
export const Route = createFileRoute("/auth/callback")({
component: OAuthCallbackPage,
});
  • Add providers — add objects to the providers array with { name, label, url }
  • Remove passkeys — delete the PasskeyButtonBase section
  • Remove OAuth — delete the OAuthButtonsBase section and OAuth config
  • Custom redirect — pass redirectTo to login(): login({ email, password, redirectTo: "/onboarding" })
  • Email verification — after registration, redirect to a verification page that calls snap.useVerifyEmail()