From 0a35b58cd988721fc3869e15410ce6a5b7bfb7fc Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Sat, 16 May 2026 03:15:33 +0200 Subject: [PATCH] fix(native): human error messages + kind override checkbox in AddDomainSheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/blocker/AddDomainSheet.tsx | 69 +++++++++++++++++-- apps/rebreak-native/lib/api.ts | 26 ++++++- apps/rebreak-native/locales/de.json | 4 ++ apps/rebreak-native/locales/en.json | 4 ++ apps/rebreak-native/locales/fr.json | 4 ++ 5 files changed, 99 insertions(+), 8 deletions(-) diff --git a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx index 1c7bcbd..bfb77c4 100644 --- a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx +++ b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { ActivityIndicator, Image, @@ -47,13 +47,22 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { const [confirmPermanent, setConfirmPermanent] = useState(false); const [adding, setAdding] = useState(false); const [error, setError] = useState(null); + // User-Override über den Auto-Detect. null = follow auto-detect, sonst forced. + const [kindOverride, setKindOverride] = useState<'web' | 'mail' | null>(null); - const kind = detectKind(input); + const detected = detectKind(input); + const kind: 'web' | 'mail' | null = kindOverride ?? detected; const normalizedWeb = kind === 'web' ? normalizeDomain(input) : ''; const normalizedMail = kind === 'mail' ? mailDomain(input) : ''; + // Reset override sobald User komplett neuen Input tippt + useEffect(() => { + if (!input) setKindOverride(null); + }, [input]); + function close() { setInput(''); + setKindOverride(null); setConfirmPermanent(false); setError(null); onClose(); @@ -83,12 +92,23 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { } if (result.alreadyGlobal) { setError(t('blocker.add_sheet_already_global', { domain: pattern })); - } else if (result.error?.includes('WEB_LIMIT_REACHED')) { - setError(t('blocker.error_web_limit_reached')); - } else if (result.error?.includes('MAIL_LIMIT_REACHED')) { - setError(t('blocker.error_mail_limit_reached')); } else { - setError(result.error ?? t('blocker.add_sheet_add_failed')); + const raw = (result.error ?? '').toLowerCase(); + if (raw.includes('web_limit_reached')) { + setError(t('blocker.error_web_limit_reached')); + } else if (raw.includes('mail_limit_reached')) { + setError(t('blocker.error_mail_limit_reached')); + } else if (raw.includes('invalid_mail_domain') || raw.includes('display_name_not_supported')) { + setError(t('blocker.error_invalid_mail')); + } else if (raw.includes('invalid_domain') || raw.includes('invalid_pattern')) { + setError(t('blocker.error_invalid_input')); + } else if (raw.includes('eintrag bereits vorhanden') || raw.includes('duplicate')) { + setError(t('blocker.error_duplicate')); + } else { + // Letzter Fallback: niemals raw JSON anzeigen. Wenn message-Feld da war, kommt's + // sauber als String an — sonst generic Fehler. + setError(t('blocker.add_sheet_add_failed')); + } } } @@ -182,6 +202,41 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { t={t} /> + {/* Override toggle — User kann Auto-Detect korrigieren falls falsch erkannt */} + {detected !== null && ( + setKindOverride(kind === 'mail' ? 'web' : 'mail')} + activeOpacity={0.7} + style={{ + flexDirection: 'row', + alignItems: 'center', + gap: 10, + paddingHorizontal: 12, + paddingVertical: 10, + borderRadius: 12, + borderWidth: 1, + borderColor: colors.border, + backgroundColor: colors.surface, + }} + > + + + {t('blocker.kind_override_label')} + + + )} + {/* Warning card */} ( } catch {} } - throw new Error(`API ${res.status}: ${text}`); + // 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(); diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index 6274109..e8c0665 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -338,6 +338,10 @@ "count_label": "%{count}/%{max}", "error_web_limit_reached": "Du hast alle Domain-Slots aufgebraucht. Entferne eine Domain oder upgrade auf Pro/Legend.", "error_mail_limit_reached": "Du hast alle Mail-Slots aufgebraucht. Entferne ein Mail-Pattern oder upgrade auf Pro/Legend.", + "error_invalid_mail": "Bitte eine vollständige Mail-Adresse oder Mail-Domain eingeben (z.B. info@only4-subscribers.com).", + "error_invalid_input": "Bitte eine gültige Domain oder Mail-Adresse eingeben.", + "error_duplicate": "Diesen Eintrag hast du schon — er ist bereits in deiner Filter-Liste.", + "kind_override_label": "Das ist eine E-Mail-Adresse / Mail-Absender", "empty_web": "Noch keine eigenen Domains.\nTippe + um eine hinzuzufügen.", "empty_mail": "Noch keine Mail-Domains. Tippe + um eine E-Mail-Adresse oder Domain zu blockieren." }, diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index e3ddb2a..07c28e3 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -338,6 +338,10 @@ "count_label": "%{count}/%{max}", "error_web_limit_reached": "You've used all your domain slots. Remove a domain or upgrade to Pro/Legend.", "error_mail_limit_reached": "You've used all your email slots. Remove an email pattern or upgrade to Pro/Legend.", + "error_invalid_mail": "Please enter a full email address or mail domain (e.g. info@only4-subscribers.com).", + "error_invalid_input": "Please enter a valid domain or email address.", + "error_duplicate": "You've already added this entry — it's in your filter list.", + "kind_override_label": "This is an email address / mail sender", "empty_web": "No custom domains yet.\nTap + to add one.", "empty_mail": "No mail domains yet. Tap + to block an email address or domain." }, diff --git a/apps/rebreak-native/locales/fr.json b/apps/rebreak-native/locales/fr.json index 4ffea53..441a13a 100644 --- a/apps/rebreak-native/locales/fr.json +++ b/apps/rebreak-native/locales/fr.json @@ -338,6 +338,10 @@ "count_label": "%{count}/%{max}", "error_web_limit_reached": "Vous avez utilisé tous vos emplacements de domaines. Supprimez un domaine ou passez à Pro/Legend.", "error_mail_limit_reached": "Vous avez utilisé tous vos emplacements e-mail. Supprimez un modèle ou passez à Pro/Legend.", + "error_invalid_mail": "Veuillez saisir une adresse e-mail complète ou un domaine mail (ex. info@only4-subscribers.com).", + "error_invalid_input": "Veuillez saisir un domaine ou une adresse e-mail valide.", + "error_duplicate": "Vous avez déjà ajouté cette entrée — elle est dans votre liste de filtres.", + "kind_override_label": "C'est une adresse e-mail / expéditeur mail", "empty_web": "Aucun domaine personnalisé.\nAppuyez sur + pour en ajouter un.", "empty_mail": "Aucun domaine mail. Appuyez sur + pour bloquer une adresse ou un domaine." },