import Constants from "expo-constants"; import { useAuth } from "./auth"; /** * API-klient. Mobilappen pratar ENDAST med Food API – aldrig direkt med * modelleverantörer, och innehåller inga AI-hemligheter (spec §31, §61.13). * 401 → automatisk refresh med roterande token → retry en gång. */ const API_BASE = (Constants.expoConfig?.extra as { apiBaseUrl?: string } | undefined)?.apiBaseUrl ?? "http://localhost:4000"; export class ApiError extends Error { constructor( public readonly status: number, public readonly code: string, message: string, public readonly details?: unknown, ) { super(message); } } let refreshPromise: Promise | null = null; async function tryRefresh(): Promise { if (!refreshPromise) { refreshPromise = (async () => { const { getRefreshToken, setSession, logout } = useAuth.getState(); const refreshToken = await getRefreshToken(); if (!refreshToken) return false; try { const res = await fetch(`${API_BASE}/v1/auth/refresh`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ refreshToken }), }); if (!res.ok) { await logout(); return false; } const json = (await res.json()) as { accessToken: string; refreshToken: string }; await setSession( { accessToken: json.accessToken, refreshToken: json.refreshToken }, useAuth.getState().user, ); return true; } catch { return false; } finally { setTimeout(() => { refreshPromise = null; }, 100); } })(); } return refreshPromise; } export async function api( path: string, options: { method?: string; body?: unknown; retry?: boolean } = {}, ): Promise { const { accessToken } = useAuth.getState(); const res = await fetch(`${API_BASE}${path}`, { method: options.method ?? "GET", headers: { "content-type": "application/json", ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}), }, body: options.body != null ? JSON.stringify(options.body) : undefined, }); if (res.status === 401 && options.retry !== false) { const refreshed = await tryRefresh(); if (refreshed) return api(path, { ...options, retry: false }); } const json = (await res.json().catch(() => ({}))) as { error?: { code?: string; message?: string; details?: unknown }; }; if (!res.ok) { throw new ApiError( res.status, json.error?.code ?? "UNKNOWN", json.error?.message ?? `HTTP ${res.status}`, json.error?.details, ); } return json as T; } /** Ladda upp en bild till en presignad URL (mock-S3 i dev, riktig S3 i prod). */ export async function uploadImage( upload: { uploadUrl: string; headers: Record }, localUri: string, ): Promise { const blob = await (await fetch(localUri)).blob(); const res = await fetch(upload.uploadUrl, { method: "PUT", headers: upload.headers, body: blob, }); if (!res.ok) throw new ApiError(res.status, "UPLOAD_FAILED", "Bilduppladdningen misslyckades."); } export { API_BASE };