// Session — minimal auth scaffold backed by localStorage. // Swap loadSession/signIn/signOut for real calls (cookies + server) when you // wire a backend. The shape here matches what AppShell + useUser expect. import { useEffect, useSyncExternalStore } from "react" import { profileInitials } from "~/lib/profile" export type Session = { userId: string name: string email: string token: string // Issued at, ms since epoch. issuedAt: number } const STORAGE_KEY = "crema.session" const CHANGE_EVENT = "crema:session-change" function readFromStorage(): Session | null { if (typeof window === "undefined") return null try { const raw = localStorage.getItem(STORAGE_KEY) if (!raw) return null const parsed = JSON.parse(raw) as Partial if ( typeof parsed.userId !== "string" || typeof parsed.email !== "string" || typeof parsed.token !== "string" ) return null return { userId: parsed.userId, name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name : parsed.email, email: parsed.email, token: parsed.token, issuedAt: typeof parsed.issuedAt === "number" ? parsed.issuedAt : Date.now(), } } catch { return null } } export function loadSession(): Session | null { return readFromStorage() } /** * Mock sign-in. Validates only that email + password are non-empty; returns * a fake session. Replace with a real fetch to your auth endpoint. */ export async function signIn( email: string, password: string, ): Promise { await new Promise((r) => setTimeout(r, 250)) if (!email.trim() || !password.trim()) { throw new Error("Email and password are required.") } if (!email.includes("@")) { throw new Error("Enter a valid email address.") } const session: Session = { userId: `u-${Date.now().toString(36)}`, name: email.split("@")[0].replace(/\W/g, " ").trim() || email, email, token: `dev-${Math.random().toString(36).slice(2, 14)}`, issuedAt: Date.now(), } if (typeof window !== "undefined") { localStorage.setItem(STORAGE_KEY, JSON.stringify(session)) window.dispatchEvent(new CustomEvent(CHANGE_EVENT)) } return session } export function signOut() { if (typeof window === "undefined") return localStorage.removeItem(STORAGE_KEY) sessionStorage.removeItem("arcadia_access_token") sessionStorage.removeItem("arcadia_refresh_token") window.dispatchEvent(new CustomEvent(CHANGE_EVENT)) } /** Bridge: persist a Session record from a successful arcadia login. * Stores the JWT in sessionStorage (where ArcadiaProvider's getToken reads * it) and writes the user-shaped Session into localStorage so the existing * AppShell / useUser machinery keeps working unchanged. */ export function persistFromArcadiaLogin( tokens: { access_token: string; refresh_token?: string }, user?: { id: string; email: string; full_name?: string; first_name?: string; last_name?: string } | null, ): Session { const name = user?.full_name || [user?.first_name, user?.last_name].filter(Boolean).join(" ") || user?.email || "Signed-in user" const session: Session = { userId: user?.id ?? `arcadia-${Date.now().toString(36)}`, name, email: user?.email ?? "", token: tokens.access_token, issuedAt: Date.now(), } if (typeof window !== "undefined") { sessionStorage.setItem("arcadia_access_token", tokens.access_token) if (tokens.refresh_token) sessionStorage.setItem("arcadia_refresh_token", tokens.refresh_token) localStorage.setItem(STORAGE_KEY, JSON.stringify(session)) window.dispatchEvent(new CustomEvent(CHANGE_EVENT)) } return session } /** True if a non-expired session is in storage. */ export function hasSession(): boolean { return !!readFromStorage() } let cached: Session | null = null let cacheValid = false function subscribe(cb: () => void): () => void { const onChange = () => { cacheValid = false cb() } window.addEventListener(CHANGE_EVENT, onChange) window.addEventListener("storage", (e) => { if (e.key === STORAGE_KEY) onChange() }) return () => window.removeEventListener(CHANGE_EVENT, onChange) } function getSnapshot(): Session | null { if (!cacheValid) { cached = readFromStorage() cacheValid = true } return cached } function getServerSnapshot(): Session | null { return null } export function useSession(): Session | null { const s = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) useEffect(() => { cacheValid = false }, []) return s } export function sessionInitials(session: Session | null): string { if (!session) return "?" return profileInitials(session.name || session.email) }