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 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; }