import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from 'react' import { ApiError, getAccessToken, isAbortError } from '../lib/api' import { setActiveBusinessById } from '../lib/businessContext' import { getTenantDomain } from '../lib/config' import { fetchCurrentUser, login as loginRequest, loginWithOtp as loginWithOtpRequest, logout as logoutRequest, } from '../services/authService' import { resolveTenantByDomain } from '../services/tenantService' import type { AuthUser } from '../types/auth' interface AuthContextValue { user: AuthUser | null isLoading: boolean login: (cellNumber: string, password: string) => Promise loginWithOtp: (cellNumber: string, code: string) => Promise logout: () => void setUser: (user: AuthUser) => void } const AuthContext = createContext(null) export const CUSTOMER_ACCESS_MESSAGE = 'This account cannot access the customer dashboard. Please sign in with a verified customer account.' export const VERIFICATION_REQUIRED_MESSAGE = 'Your mobile number is not verified yet. Please complete SMS verification before accessing the dashboard.' function isVerifiedUser(user: AuthUser) { return user.cellVerifiedAt !== null && user.cellVerifiedAt !== undefined } function canAccessCustomerDashboard(user: AuthUser) { return user.customerBusinesses.length > 0 } async function prepareCustomerContext(user: AuthUser) { const domain = getTenantDomain() const tenant = await resolveTenantByDomain(domain) const membership = user.customerBusinesses.find((business) => business.id === tenant.id) ?? user.customerBusinesses.find((business) => business.slug === tenant.slug) ?? user.customerBusinesses[0] if (!membership) { throw new ApiError('You are not a customer of this business.', 403) } setActiveBusinessById(String(membership.id), domain) } async function assertCustomerAccess(user: AuthUser) { if (!canAccessCustomerDashboard(user)) { logoutRequest() throw new ApiError(CUSTOMER_ACCESS_MESSAGE, 403) } if (!isVerifiedUser(user)) { logoutRequest() throw new ApiError(VERIFICATION_REQUIRED_MESSAGE, 403) } await prepareCustomerContext(user) } export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) const [isLoading, setIsLoading] = useState(true) const logout = useCallback(() => { logoutRequest() setUser(null) }, []) useEffect(() => { const controller = new AbortController() async function init() { if (!getAccessToken()) { if (!controller.signal.aborted) setIsLoading(false) return } try { const { user: currentUser } = await fetchCurrentUser(controller.signal) if (controller.signal.aborted) return if (!canAccessCustomerDashboard(currentUser) || !isVerifiedUser(currentUser)) { logout() return } await prepareCustomerContext(currentUser) if (controller.signal.aborted) return setUser(currentUser) } catch (err) { if (isAbortError(err) || controller.signal.aborted) 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, getTenantDomain()) await assertCustomerAccess(data.user) setUser(data.user) }, []) const loginWithOtp = useCallback(async (cellNumber: string, code: string) => { const data = await loginWithOtpRequest(cellNumber, code, getTenantDomain()) await assertCustomerAccess(data.user) setUser(data.user) }, []) const value = useMemo( () => ({ user, isLoading, login, loginWithOtp, logout, setUser }), [user, isLoading, login, loginWithOtp, logout], ) return {children} } export function useAuth() { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within AuthProvider') } return context }