import { api } from "@/services/api"; import type { ProfileTypeValue } from "@/services/companies.service"; import { companiesService } from "@/services/companies.service"; import type { LoginPayload, LoginResponse, OtpResponse, SignupPayload, SignupResponse, } from "@/types/auth"; import type { Result } from "@/utils/result"; import { extractApiError } from "@/utils/result"; import { useQuery, useQueryClient } from "@tanstack/react-query"; function setCookie(name: string, value: string, days: number) { const expires = new Date(); expires.setDate(expires.getDate() + days); document.cookie = `${name}=${value}; path=/; expires=${expires.toUTCString()}; SameSite=Lax`; } function getCookie(name: string): string | undefined { return document.cookie .split("; ") .find((row) => row.startsWith(`${name}=`)) ?.split("=")[1]; } const useAuth = () => { const queryClient = useQueryClient(); const hasToken = !!getCookie("auth-token"); const authQuery = useQuery( api.auth.getMyInfo.queryOptions({ enabled: hasToken, retry: false, refetchOnMount: false, refetchOnReconnect: false, staleTime: 10 * 60 * 1000, refetchOnWindowFocus: false, }), ); const companyQuery = useQuery( api.companies.getInfo.queryOptions({ enabled: !!authQuery.data?.id, retry: false, staleTime(query) { // Fast-poll while anything is awaiting a backoffice decision: an // unapproved role, or a pending profile-edit review. This surfaces // approvals/rejections to the portal within a minute. if ( query.state.data?.review?.status === "pending" || query.state.data?.company?.companyProfiles?.find( (p) => p.status !== "active", ) ) return 60; return 10 * 60 * 1000; }, refetchOnWindowFocus: false, }), ); const isPending = authQuery.isPending && hasToken; const isAuthenticated = hasToken && !!authQuery.data && !authQuery.isError; const login = async ( payload: LoginPayload, ): Promise> => { try { const res = await api.auth.login.call(payload); setCookie("auth-token", res.token, 7); setCookie("refresh-token", res.refreshToken, 7); await authQuery.refetch(); return { success: true, data: res }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; const signup = async ( payload: SignupPayload, ): Promise> => { try { const res = await api.auth.createUser.call(payload); setCookie("auth-token", res.token, 7); setCookie("refresh-token", res.refreshToken, 7); await authQuery.refetch(); return { success: true, data: res }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; const setPassword = async (data: { newPassword: string; confirmPassword: string; }): Promise> => { try { const userId = authQuery.data?.id ?? ""; const email = localStorage.getItem("otp-email") ?? ""; const verificationCode = localStorage.getItem("otp") ?? ""; await api.auth.setPassword.call({ newPassword: data.newPassword, confirmPassword: data.confirmPassword, userId, email, verificationCode, }); ["userId", "otp", "otp-phone", "otp-email"].forEach((k) => localStorage.removeItem(k), ); await queryClient.invalidateQueries({ queryKey: api.auth.getMyInfo.queryKey(), }); return { success: true, data: undefined }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; const verifyOTP = async (otp: string): Promise> => { try { const phone = localStorage.getItem("otp-phone") ?? ""; const res = await api.auth.verifyOTP.call({ phone, otp }); return { success: true, data: res }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; const sendOTP = async (otp: string): Promise> => { try { const phone = localStorage.getItem("otp-phone") ?? ""; const res = await api.auth.sendOTP.call({ phone, otp }); return { success: true, data: res }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; const generateVerificationCode = async ( type: string, ): Promise> => { try { const email = localStorage.getItem("otp-email") ?? ""; const phoneNumber = localStorage.getItem("otp-phone") ?? ""; const res = await api.auth.generateVerificationCode.call({ email, phoneNumber, type, }); return { success: true, data: res }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; // Active-mode (importer/exporter) state, sourced from the persisted profile. const companyInfo = isAuthenticated ? (companyQuery.data ?? null) : null; const activeProfileType = companyInfo?.profile?.activeProfileType ?? null; const activeCompanyProfileId = companyInfo?.profile?.activeCompanyProfileId ?? null; const companyType = companyInfo?.company?.type ?? null; const companyStatus = companyInfo?.company?.status ?? null; // A company can create bookings only once an admin has approved it (active). const isCompanyApproved = companyStatus === "active"; const onboardingCompleted = companyInfo?.profile?.onboardingCompleted ?? false; const onboardingStep = companyInfo?.profile?.onboardingStep ?? null; // Booking is gated on backoffice approval of the active operational profile: // a customer can only book under a profile once its status is "active". const activeProfile = companyInfo?.company?.companyProfiles?.find( (p) => p.id === activeCompanyProfileId, ) ?? null; const activeProfileStatus = activeProfile?.status ?? null; const canBook = activeProfileStatus === "active"; // Profile-edit review: while a change request is pending the customer is // locked out of editing and of creating new contracts/bookings; a rejected // request surfaces the reviewer note so they can amend and resubmit. const review = companyInfo?.review ?? null; const reviewStatus = review?.status ?? null; const reviewNote = review?.note ?? null; const isUnderReview = reviewStatus === "pending"; /** Refetch everything scoped to the active operational profile. */ const invalidateScopedData = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: api.companies.getInfo.queryKey(), }), queryClient.invalidateQueries({ queryKey: api.companies.getDashboard.queryKey(), }), queryClient.invalidateQueries({ queryKey: ["bookings"] }), ]); }; const switchMode = async (type: ProfileTypeValue): Promise> => { try { await api.companies.setActiveMode.call({ type }); await invalidateScopedData(); return { success: true, data: undefined }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; /** * Add an operational role. The new role starts pending review, so the active * mode is left untouched — the user keeps working under their approved role. */ const createProfile = async ( type: ProfileTypeValue, licenseFiles: File[], ): Promise> => { try { const created = await api.companies.createCompanyProfile.call({ type }); if (licenseFiles.length > 0) { await companiesService.uploadProfileLicense(created.id, licenseFiles); } await invalidateScopedData(); return { success: true, data: undefined }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; /** * Resubmit a rejected operational role for approval — optionally replacing its * business license first (the common reason a role is rejected) — then refresh. */ const reapplyProfile = async ( profileId: string, licenseFiles: File[] = [], ): Promise> => { try { if (licenseFiles.length > 0) { await companiesService.uploadProfileLicense(profileId, licenseFiles); } await api.companies.reapplyProfile.call({ profileId }); await invalidateScopedData(); return { success: true, data: undefined }; } catch (err) { return { success: false, error: extractApiError(err) }; } }; const logout = async () => { try { await api.auth.logout.call(); } catch { // proceed with client-side cleanup even if server call fails } [ "auth-token", "refresh-token", "auth-user", "current-position-id", "selected-position-id", ].forEach((name) => { document.cookie = `${name}=; Max-Age=0; path=/`; }); localStorage.clear(); queryClient.clear(); }; return { isPending, isAuthenticated, user: isAuthenticated ? (authQuery.data ?? null) : null, company: isAuthenticated ? (companyQuery.data ?? null) : null, customer: isAuthenticated ? (companyQuery.data ?? null) : null, activeProfileType, activeCompanyProfileId, activeProfileStatus, canBook, companyType, companyStatus, isCompanyApproved, reviewStatus, reviewNote, isUnderReview, onboardingCompleted, onboardingStep, switchMode, createProfile, reapplyProfile, login, signup, setPassword, verifyOTP, sendOTP, generateVerificationCode, logout, authQuery, companyQuery, customerQuery: companyQuery, }; }; export default useAuth;