chahinebrini 0a35b58cd9 fix(native): human error messages + kind override checkbox in AddDomainSheet
Two related fixes after the user saw a raw 400 JSON dump in the sheet
("API 400: { error: true, message: 'Eintrag bereits vorhanden' … }").

1. apiFetch now extracts the prettiest available message from the
   response body (data.message → message → statusMessage → raw text →
   bare status code) and throws an Error whose .message is that string
   only. Stashes the structured pieces on the Error too (.code, .data,
   .status) so callers that switch on error codes still have them, but
   the default `e?.message` path delivers a clean human sentence.
2. AddDomainSheet maps the known error codes to localized strings —
   WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED / INVALID_MAIL_DOMAIN /
   DISPLAY_NAME_NOT_SUPPORTED / INVALID_DOMAIN / "Eintrag bereits
   vorhanden" (duplicate) — and falls back to a generic copy if the
   code is unknown. The raw API JSON never appears in the UI again.

Plus the kind-override checkbox: the auto-detect (input contains "@" →
mail, contains "." → web) is fine for the typical case but a user can
type a clean domain and still want it filtered against mail senders
(e.g. they know "casino.de" is also their casino's sender domain).
The new pill below the preview toggles between mail and web, defaults
to whatever auto-detect said, and resets when the input is cleared. The
local-part strip still runs for mail-mode so the stored value stays a
domain.

i18n: error_invalid_mail / error_invalid_input / error_duplicate /
kind_override_label across DE/EN/FR.
2026-05-16 03:15:33 +02:00

115 lines
3.6 KiB
TypeScript

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<RequestInit, 'body'> & {
body?: any;
/** Set true on bootstrap calls (device register) to skip x-device-id injection */
skipDeviceHeader?: boolean;
};
let cachedDeviceHeaders: Record<string, string> | null = null;
async function getDeviceHeaders(): Promise<Record<string, string>> {
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<T = any>(
path: string,
options: FetchOptions = {}
): Promise<T> {
const session = (await supabase.auth.getSession()).data.session;
const { skipDeviceHeader, ...fetchOptions } = options;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(fetchOptions.headers as Record<string, string>),
};
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;
}