"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import { ApiError, getAccessToken, isAbortError, migrateTokensToSharedCookies } from "@/lib/meshkee-api"; import { getAdminDashboardOrigin, getCustomerDashboardOrigin, hostAppFor, type HostApp, } from "@/lib/host"; import { clearManagedBusinessId, getManagedBusinessId, setManagedBusinessId, } from "@/lib/managed-business"; import { fetchCurrentUser, login as loginRequest, logout as logoutRequest, register as registerRequest, type RegisterPayload, } from "@/services/auth"; import type { AuthUser, DashboardType } from "@/types/auth"; interface AuthContextValue { user: AuthUser | null; isLoading: boolean; activeBusinessId: string | null; isBusinessOwner: boolean; isSuperAdmin: boolean; login: (cellNumber: string, password: string) => Promise; register: (payload: RegisterPayload) => Promise; logout: () => void; } const AuthContext = createContext(null); export function pickActiveBusinessId(user: AuthUser): string | null { const owned = user.businesses.find((item) => item.isOwner); const first = owned ?? user.businesses[0]; return first ? String(first.id) : null; } export function userCanAccessApp(user: AuthUser, app: HostApp): boolean { if (app === "admin") return user.dashboard === "super_admin"; if (app === "customer") { // Shared entry: customer, business owners, and superadmins all land here first. return ( user.dashboard === "customer" || user.dashboard === "business" || user.dashboard === "super_admin" ); } // Business dashboard: owners/staff, or superadmin (via managed business id). if (user.dashboard === "super_admin") return true; return Boolean(pickActiveBusinessId(user)); } function currentHost(): string { if (typeof window === "undefined") return ""; return window.location.host; } /** Prefer ?businessId= on the URL, then stored managed id. */ function syncManagedBusinessFromUrl() { if (typeof window === "undefined") return getManagedBusinessId(); const fromUrl = new URLSearchParams(window.location.search) .get("businessId") ?.trim(); if (fromUrl) { setManagedBusinessId(fromUrl); return fromUrl; } return getManagedBusinessId(); } export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const [managedBusinessId, setManagedBusinessIdState] = useState( null, ); const logout = useCallback(() => { logoutRequest(); clearManagedBusinessId(); setManagedBusinessIdState(null); setUser(null); }, []); useEffect(() => { const controller = new AbortController(); async function init() { // Handoff installs tokens via inline script and redirects — don't race it. if ( typeof window !== "undefined" && window.location.pathname === "/auth/handoff" ) { if (!controller.signal.aborted) setIsLoading(false); return; } migrateTokensToSharedCookies(); setManagedBusinessIdState(syncManagedBusinessFromUrl()); if (!getAccessToken()) { if (!controller.signal.aborted) setIsLoading(false); return; } try { const { user: currentUser } = await fetchCurrentUser(controller.signal); if (controller.signal.aborted) return; const app = hostAppFor(currentHost()); if (!userCanAccessApp(currentUser, app)) { logout(); return; } const managed = syncManagedBusinessFromUrl(); setManagedBusinessIdState(managed); setUser(currentUser); } catch (err) { if (isAbortError(err) || controller.signal.aborted) return; // On business host, keep tokens so a transient /me failure doesn't // bounce manage-business back to admin. if (hostAppFor(currentHost()) === "business") { if (!controller.signal.aborted) setIsLoading(false); return; } logout(); } finally { if (!controller.signal.aborted) setIsLoading(false); } } void init(); return () => controller.abort(); }, [logout]); const login = useCallback(async (cellNumber: string, password: string) => { const data = await loginRequest(cellNumber, password); const app = hostAppFor(currentHost()); if (!userCanAccessApp(data.user, app)) { logoutRequest(); const message = app === "admin" ? "This account does not have super admin access." : app === "business" ? "No business is linked to this account." : "This account cannot use the customer dashboard."; throw new ApiError(message, 403); } if (app === "business" && data.user.dashboard === "super_admin") { const managed = syncManagedBusinessFromUrl(); setManagedBusinessIdState(managed); if (!managed && !pickActiveBusinessId(data.user)) { logoutRequest(); throw new ApiError( "Open a business from the admin dashboard and use Manage business.", 403, ); } } setManagedBusinessIdState(syncManagedBusinessFromUrl()); setUser(data.user); return data.user.dashboard; }, []); const register = useCallback(async (payload: RegisterPayload) => { const data = await registerRequest(payload); if (!data.accessToken || !data.refreshToken || !data.user) { throw new ApiError( data.message || "Registration needs phone verification before you can continue.", 403, ); } const app = hostAppFor(currentHost()); if (!userCanAccessApp(data.user, app)) { logoutRequest(); throw new ApiError( "This account cannot use the customer dashboard.", 403, ); } setManagedBusinessIdState(syncManagedBusinessFromUrl()); setUser(data.user); return data.user.dashboard; }, []); const isSuperAdmin = user?.dashboard === "super_admin"; /** * Shared business.* host — which Meshkee business? * - Superadmin: managed id from “Manage business” handoff (required for other businesses) * - Everyone else: first owned / linked business on the user */ const activeBusinessId = useMemo(() => { if (!user) return null; if (user.dashboard === "super_admin") { return managedBusinessId || pickActiveBusinessId(user); } return pickActiveBusinessId(user); }, [user, managedBusinessId]); const isBusinessOwner = useMemo( () => Boolean(user?.businesses.some((item) => item.isOwner)), [user], ); const value = useMemo( () => ({ user, isLoading, activeBusinessId, isBusinessOwner, isSuperAdmin: Boolean(isSuperAdmin), login, register, logout, }), [ user, isLoading, activeBusinessId, isBusinessOwner, isSuperAdmin, login, register, logout, ], ); return {children}; } export function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within AuthProvider"); } return context; } /** * After login: stay on the host you signed into. * Superadmin can open the admin dashboard from customer home when needed. */ export function postLoginHref(dashboard: DashboardType): string { const app = hostAppFor(currentHost()); if (app === "admin") { return dashboard === "super_admin" ? "/" : `${getCustomerDashboardOrigin()}/`; } if (app === "business") return "/profile"; if (app === "customer") return "/"; if (dashboard === "super_admin") return `${getAdminDashboardOrigin()}/`; return `${getCustomerDashboardOrigin()}/`; }