import React, { createContext, useContext, useEffect } from "react"; import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { meAtom } from "../api/account-atoms"; // --------------------------------------------------------------------------- // Shared auth seam for the multiplayer apps (cloud + self-host). // // `useAuth()` reflects the `/account/me` query: loading → unauthenticated → // authenticated. Provider-neutral — the only difference between cloud (WorkOS) // and self-host (Better Auth) is which server answers `me` and how the session // cookie was minted. Analytics stay OUT of here; a host that wants to identify // the user (cloud → PostHog) passes an `onIdentify` callback. // --------------------------------------------------------------------------- export type AuthUser = { id: string; email: string; name: string | null; avatarUrl: string | null; }; export type AuthOrganization = { id: string; name: string; }; export type AuthState = | { status: "loading" } | { status: "unauthenticated" } | { status: "authenticated"; user: AuthUser; organization: AuthOrganization | null }; export type IdentifyFn = ( state: Extract | { status: "unauthenticated" }, ) => void; const AuthContext = createContext({ status: "loading" }); export const useAuth = () => useContext(AuthContext); const AuthProviderClient = ({ children, onIdentify, }: { children: React.ReactNode; onIdentify?: IdentifyFn; }) => { const result = useAtomValue(meAtom); const state: AuthState = AsyncResult.match(result, { onInitial: () => ({ status: "loading" as const }), onSuccess: ({ value }) => ({ status: "authenticated" as const, user: value.user, organization: value.organization, }), onFailure: () => ({ status: "unauthenticated" as const }), }); // Primitive identity fields so the identify effect fires only on real // transitions (the `state` object is rebuilt every render). const status = state.status; const userId = state.status === "authenticated" ? state.user.id : null; const email = state.status === "authenticated" ? state.user.email : null; const name = state.status === "authenticated" ? state.user.name : null; const avatarUrl = state.status === "authenticated" ? state.user.avatarUrl : null; const organizationId = state.status === "authenticated" ? (state.organization?.id ?? null) : null; const organizationName = state.status === "authenticated" ? (state.organization?.name ?? null) : null; useEffect(() => { if (!onIdentify) return; if (status === "authenticated" && userId && email !== null) { onIdentify({ status: "authenticated", user: { id: userId, email, name, avatarUrl }, organization: organizationId ? { id: organizationId, name: organizationName ?? "" } : null, }); } else if (status === "unauthenticated") { onIdentify({ status: "unauthenticated" }); } }, [onIdentify, status, userId, email, name, avatarUrl, organizationId, organizationName]); return {children}; }; export const AuthProvider = ({ children, onIdentify, }: { children: React.ReactNode; onIdentify?: IdentifyFn; }) => { if (typeof window === "undefined") { return {children}; } return {children}; };