import Constants from 'expo-constants'; import { supabase } from './supabase'; import { getDeviceId, getPlatformName } 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; }; /** * 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 deviceId = await getDeviceId().catch(() => null); if (deviceId) { headers['x-device-id'] = deviceId; headers['x-platform'] = getPlatformName(); } } 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 new Error(`API ${res.status}: ${text}`); } 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; }