// Session — minimal auth scaffold backed by localStorage. // Sign-in is owned by `persistFromArcadiaLogin`, which is called by the auth // routes after a successful arcadia API exchange. 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() } 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 } /** Patch the stored session's identity fields without changing the token. * Use after the operator edits their profile so the appbar avatar and * protected-shell greeting reflect the new name/email immediately. */ export function updateSessionUser(patch: { name?: string email?: string }): Session | null { if (typeof window === "undefined") return null const current = readFromStorage() if (!current) return null const next: Session = { ...current, name: patch.name?.trim() ? patch.name : current.name, email: patch.email?.trim() ? patch.email : current.email, } localStorage.setItem(STORAGE_KEY, JSON.stringify(next)) window.dispatchEvent(new CustomEvent(CHANGE_EVENT)) return next } /** 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) }