import Constants from 'expo-constants'; import { supabase } from './supabase'; import { getDeviceInfo } from './deviceId'; import { useDeviceLimitStore } from '../stores/deviceLimit'; const apiUrl = Constants.expoConfig?.extra?.apiUrl as string; type FetchOptions = Omit & { body?: any; /** Set true on bootstrap calls (device register) to skip x-device-id injection */ skipDeviceHeader?: boolean; }; let cachedDeviceHeaders: Record | null = null; async function getDeviceHeaders(): Promise> { if (cachedDeviceHeaders) return cachedDeviceHeaders; const info = await getDeviceInfo().catch(() => null); if (!info) return {}; cachedDeviceHeaders = { 'x-device-id': info.deviceId, 'x-platform': info.platform, 'x-device-name': encodeURIComponent(info.name), 'x-device-model': encodeURIComponent(info.model), 'x-device-os': encodeURIComponent(info.osVersion), }; return cachedDeviceHeaders; } /** * Wrapper für Backend-API-Calls mit automatischem Auth-Token. * Pendant zum Nuxt-`useSafeFetch` aus apps/rebreak/. * * Backend antwortet mit { success, data, status } — wir entpacken `data`. */ export async function apiFetch( path: string, options: FetchOptions = {} ): Promise { const session = (await supabase.auth.getSession()).data.session; const { skipDeviceHeader, ...fetchOptions } = options; const headers: Record = { 'Content-Type': 'application/json', ...(fetchOptions.headers as Record), }; if (session?.access_token) { headers.Authorization = `Bearer ${session.access_token}`; } if (!skipDeviceHeader) { const deviceHeaders = await getDeviceHeaders(); Object.assign(headers, deviceHeaders); } const res = await fetch(`${apiUrl}${path}`, { ...fetchOptions, headers, body: fetchOptions.body ? JSON.stringify(fetchOptions.body) : undefined, }); if (!res.ok) { const text = await res.text(); if (res.status === 403) { try { const parsed = JSON.parse(text); if ( parsed?.statusMessage === 'device_limit_reached' || parsed?.data?.error === 'device_limit_reached' ) { const { devices, max, plan } = parsed.data; useDeviceLimitStore.getState().show(devices ?? [], max ?? 0, plan ?? 'free'); } } catch {} } // Throw a human-readable Error.message. Backend `createError({ data: { error, message } })` // serialises into { error: true, statusCode, statusMessage, message?, data: {…} }. // Caller code only ever displays `e.message`, so collapse the prettiest field into // it; stash the rest on the Error so callers that want to switch on error_code // (e.g. WEB_LIMIT_REACHED, ALREADY_GLOBAL) can still inspect `(e as any).code`. let humanMessage = `API ${res.status}`; let errorCode: string | undefined; let errorData: any; try { const parsed = JSON.parse(text); errorData = parsed?.data ?? parsed; errorCode = errorData?.error ?? parsed?.statusMessage; humanMessage = errorData?.message ?? parsed?.message ?? parsed?.statusMessage ?? humanMessage; } catch { if (text) humanMessage = text; } const err = new Error(humanMessage); (err as any).status = res.status; (err as any).code = errorCode; (err as any).data = errorData; throw err; } const json = await res.json(); // Unwrap { success, data, status } — siehe useSafeFetch-Pattern in der Vue-App if (json && typeof json === 'object' && 'success' in json && 'data' in json) { return json.data as T; } return json as T; }