import React, { createContext, useContext, useEffect } from "react"; import * as Atom from "effect/unstable/reactivity/Atom"; import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { usePostHog } from "posthog-js/react"; import { ReactivityKey } from "@executor-js/react/api/reactivity-keys"; import { CloudApiClient } from "./client"; // --------------------------------------------------------------------------- // Types (from CloudAuthApi response schema) // --------------------------------------------------------------------------- type AuthUser = { id: string; email: string; name: string | null; avatarUrl: string | null; }; type AuthOrganization = { id: string; name: string; }; // --------------------------------------------------------------------------- // Auth atom — typed query against CloudAuthApi // --------------------------------------------------------------------------- export const authAtom = CloudApiClient.query("cloudAuth", "me", { timeToLive: "5 minutes", reactivityKeys: [ReactivityKey.auth], }); export const organizationsAtom = Atom.refreshOnWindowFocus( CloudApiClient.query("cloudAuth", "organizations", { timeToLive: "1 minute", reactivityKeys: [ReactivityKey.auth], }), ); export const switchOrganization = CloudApiClient.mutation("cloudAuth", "switchOrganization"); export const createOrganization = CloudApiClient.mutation("cloudAuth", "createOrganization"); export const pendingInvitationsAtom = CloudApiClient.query("cloudAuth", "pendingInvitations", { timeToLive: "1 minute", reactivityKeys: [ReactivityKey.auth], }); export const acceptInvitation = CloudApiClient.mutation("cloudAuth", "acceptInvitation"); // --------------------------------------------------------------------------- // Provider + hook // --------------------------------------------------------------------------- type AuthState = | { status: "loading" } | { status: "unauthenticated" } | { status: "authenticated"; user: AuthUser; organization: AuthOrganization | null }; const AuthContext = createContext({ status: "loading" }); export const useAuth = () => useContext(AuthContext); const AuthProviderClient = ({ children }: { children: React.ReactNode }) => { const result = useAtomValue(authAtom); const posthog = usePostHog(); 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 }), }); 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 orgId = state.status === "authenticated" ? (state.organization?.id ?? null) : null; const orgName = state.status === "authenticated" ? (state.organization?.name ?? null) : null; const isUnauthenticated = state.status === "unauthenticated"; useEffect(() => { if (!posthog) return; if (userId) { posthog.identify(userId, { email, name }); if (orgId) { posthog.group("organization", orgId, { name: orgName }); } } else if (isUnauthenticated) { posthog.reset(); } }, [posthog, userId, email, name, orgId, orgName, isUnauthenticated]); return {children}; }; export const AuthProvider = ({ children }: { children: React.ReactNode }) => { if (typeof window === "undefined") { return {children}; } return {children}; };