From 4eab5df7e2130c3edeb04e4db63175965de1cb8d Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Sat, 16 May 2026 01:54:32 +0200 Subject: [PATCH] feat(native/blocker): type picker + mail patterns in AddDomainSheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AddDomainSheet now opens with a Seite / E-Mail segmented control. Web keeps the existing flow (label, placeholder, favicon preview, domain normalization). Mail switches to a free-form pattern input (address / domain / display-name — user types what they see in their inbox) with a mail-icon preview after the field is filled. addDomain(pattern, kind) now sends { pattern, kind: 'web' | 'mail' } and the server decides the concrete type. Type field flows through the CustomDomain type so DomainGrid tiles render the mail-outline icon for mail entries instead of the favicon fallback. i18n: blocker.type_web / type_mail / add_web_* / add_mail_* across de/en/fr with %{var} placeholders per repo convention. --- apps/rebreak-native/app/(app)/blocker.tsx | 4 +- .../components/blocker/AddDomainSheet.tsx | 245 ++++++++++++++++-- .../components/blocker/DomainGrid.tsx | 17 +- apps/rebreak-native/hooks/useCustomDomains.ts | 16 +- apps/rebreak-native/locales/de.json | 11 +- apps/rebreak-native/locales/en.json | 11 +- apps/rebreak-native/locales/fr.json | 11 +- 7 files changed, 274 insertions(+), 41 deletions(-) diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx index 4a1419c..559fa84 100644 --- a/apps/rebreak-native/app/(app)/blocker.tsx +++ b/apps/rebreak-native/app/(app)/blocker.tsx @@ -387,8 +387,8 @@ export default function BlockerScreen() { setAddSheetOpen(false); refreshDomains(); }} - onAdd={async (d) => { - const result = await addDomain(d); + onAdd={async (pattern, kind) => { + const result = await addDomain(pattern, kind); if (result.ok) { // Neue Custom-Domain → Filter muss aktualisierten Hash-Set kriegen const sync = await syncBlocklist(); diff --git a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx index 6173059..bda20ab 100644 --- a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx +++ b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx @@ -13,27 +13,30 @@ import { normalizeDomain, type Tier, } from '../../hooks/useCustomDomains'; -import { useColors } from '../../lib/theme'; +import { useColors, type ColorScheme } from '../../lib/theme'; import { FormSheet } from '../FormSheet'; import { SheetFieldStack } from '../SheetFieldStack'; +type InputKind = 'web' | 'mail'; + type Props = { visible: boolean; tier: Tier; onClose: () => void; - onAdd: (domain: string) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>; + onAdd: (pattern: string, kind: InputKind) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>; }; export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { const { t } = useTranslation(); const colors = useColors(); + const [kind, setKind] = useState('web'); const [input, setInput] = useState(''); const [confirmPermanent, setConfirmPermanent] = useState(false); const [adding, setAdding] = useState(false); const [error, setError] = useState(null); const [fieldsDone, setFieldsDone] = useState(false); - const normalized = normalizeDomain(input); + const normalizedWeb = kind === 'web' ? normalizeDomain(input) : ''; function close() { setInput(''); @@ -43,18 +46,32 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { onClose(); } + function handleKindChange(next: InputKind) { + if (next === kind) return; + setKind(next); + setInput(''); + setError(null); + setFieldsDone(false); + } + + function isInputValid(): boolean { + if (kind === 'web') return isValidDomain(input); + return input.trim().length > 0; + } + async function handleAdd() { - if (!isValidDomain(input) || !confirmPermanent || adding) return; + if (!isInputValid() || !confirmPermanent || adding) return; setAdding(true); setError(null); - const result = await onAdd(input); + const pattern = kind === 'web' ? input : input.trim(); + const result = await onAdd(pattern, kind); setAdding(false); if (result.ok) { close(); return; } if (result.alreadyGlobal) { - setError(t('blocker.add_sheet_already_global', { domain: normalized })); + setError(t('blocker.add_sheet_already_global', { domain: normalizedWeb || input.trim() })); } else { setError(result.error ?? t('blocker.add_sheet_add_failed')); } @@ -65,6 +82,22 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { ? t('blocker.add_sheet_warning_free') : t('blocker.add_sheet_warning_pro'); + const inputLabel = kind === 'web' + ? t('blocker.add_web_label') + : t('blocker.add_mail_label'); + + const inputPlaceholder = kind === 'web' + ? t('blocker.add_web_placeholder') + : t('blocker.add_mail_placeholder'); + + const helpText = kind === 'web' + ? t('blocker.add_web_help') + : t('blocker.add_mail_help'); + + const validateField = kind === 'web' + ? (v: string) => isValidDomain(v) ? undefined : t('blocker.add_sheet_invalid') + : (v: string) => v.trim().length > 0 ? undefined : t('blocker.add_mail_invalid'); + return ( + } fields={[ { - key: 'domain', - label: t('blocker.add_sheet_label'), - placeholder: t('blocker.add_sheet_placeholder'), + key: 'pattern', + label: inputLabel, + placeholder: inputPlaceholder, value: input, onChangeText: (v) => { setInput(v); setError(null); }, - normalize: normalizeDomain, - keyboardType: 'url', + normalize: kind === 'web' ? normalizeDomain : undefined, + keyboardType: kind === 'web' ? 'url' : 'default', autoCapitalize: 'none', autoCorrect: false, - validate: (v) => - isValidDomain(v) ? undefined : t('blocker.add_sheet_invalid'), + validate: validateField, }, ]} onComplete={() => setFieldsDone(true)} > - {/* Favicon-Preview */} + {/* Help-Text */} - - {normalized} + {helpText} + {/* Favicon-Preview (nur Web) */} + {kind === 'web' && ( + + + + {normalizedWeb} + + + )} + + {/* Mail-Typ Icon-Preview */} + {kind === 'mail' && ( + + + + + + {input.trim() || inputPlaceholder} + + + )} + {/* Warnung */} ); } + +// ─── TypePicker ────────────────────────────────────────────────────────────── + +function TypePicker({ kind, onChange }: { kind: InputKind; onChange: (k: InputKind) => void }) { + const { t } = useTranslation(); + const colors = useColors(); + + return ( + + onChange('web')} + colors={colors} + /> + onChange('mail')} + colors={colors} + /> + + ); +} + +function TypePill({ + icon, + label, + active, + onPress, + colors, +}: { + icon: 'globe-outline' | 'mail-outline'; + label: string; + active: boolean; + onPress: () => void; + colors: ColorScheme; +}) { + return ( + + + + {label} + + + ); +} diff --git a/apps/rebreak-native/components/blocker/DomainGrid.tsx b/apps/rebreak-native/components/blocker/DomainGrid.tsx index a6eb570..2b8a660 100644 --- a/apps/rebreak-native/components/blocker/DomainGrid.tsx +++ b/apps/rebreak-native/components/blocker/DomainGrid.tsx @@ -391,9 +391,22 @@ function DomainTile({ - {/* Mitte: Favicon + Domain-Name (zentriert, flex-1) */} + {/* Mitte: Icon + Domain-Name (zentriert, flex-1) */} - {!imgError ? ( + {domain.type === 'mail_domain' || domain.type === 'mail_display_name' ? ( + + + + ) : !imgError ? ( Promise; - addDomain: (domain: string) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>; + addDomain: (domain: string, kind?: 'web' | 'mail') => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>; submitDomain: (id: string) => Promise<{ ok: boolean; error?: string }>; removeDomain: (id: string) => Promise<{ ok: boolean; error?: string }>; /** Live-Validate (regex) ob string gültiger Domain-Name ist. */ @@ -109,17 +112,16 @@ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn { }, [fetchDomains]); const addDomain = useCallback( - async (input: string) => { - if (!isValidDomain(input)) return { ok: false, error: 'invalid_domain' }; + async (input: string, kind: 'web' | 'mail' = 'web') => { + if (kind === 'web' && !isValidDomain(input)) return { ok: false, error: 'invalid_domain' }; + if (kind === 'mail' && !input.trim()) return { ok: false, error: 'invalid_pattern' }; const tier = deriveTier(plan, domains); if (tier.atLimit) return { ok: false, error: 'limit_reached' }; - const normalized = normalizeDomain(input); + const pattern = kind === 'web' ? normalizeDomain(input) : input.trim(); try { - // Backend könnte einen `alreadyGlobal`-Flag setzen wenn die Domain - // bereits in der globalen Blocklist ist (Slot wird nicht verbraucht). const res = await apiFetch('/api/custom-domains', { method: 'POST', - body: { domain: normalized }, + body: { pattern, kind }, }); if (res?.alreadyGlobal) { return { ok: false, alreadyGlobal: true }; diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index 48bfe69..c60a72d 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -316,7 +316,16 @@ "cooldown_elapsed_message": "Der Cooldown ist abgelaufen — der Schutz wurde deaktiviert. Du kannst den ReBreak-Bedienungshilfe-Dienst jetzt in den Einstellungen ausschalten.", "cooldown_elapsed_open_settings": "Einstellungen öffnen", "app_lock_coming_soon_badge": "Bald", - "app_lock_coming_soon_desc": "App-Sperre wird bald verfügbar — Schutz ist bereits aktiv." + "app_lock_coming_soon_desc": "App-Sperre wird bald verfügbar — Schutz ist bereits aktiv.", + "type_web": "Seite", + "type_mail": "E-Mail", + "add_web_label": "Domain", + "add_web_placeholder": "z.B. casino.com", + "add_web_help": "Diese Webseite wird auf allen geschützten Geräten blockiert.", + "add_mail_label": "E-Mail-Absender oder Display-Name", + "add_mail_placeholder": "z.B. only4-subscribers.com oder EXTRASPIN", + "add_mail_help": "Mail-Adresse, Domain oder Display-Name. Wir blockieren alle Mails die diesem Muster entsprechen.", + "add_mail_invalid": "Bitte ein Muster eingeben." }, "mail": { "title": "Mail-Schutz", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index 9e5e395..dac6f71 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -316,7 +316,16 @@ "cooldown_elapsed_message": "The cooldown has elapsed — protection was disabled. You can now turn off the ReBreak accessibility service in Settings.", "cooldown_elapsed_open_settings": "Open Settings", "app_lock_coming_soon_badge": "Soon", - "app_lock_coming_soon_desc": "App lock coming soon — filter protection is already active." + "app_lock_coming_soon_desc": "App lock coming soon — filter protection is already active.", + "type_web": "Website", + "type_mail": "Email", + "add_web_label": "Domain", + "add_web_placeholder": "e.g. casino.com", + "add_web_help": "This website will be blocked on all your protected devices.", + "add_mail_label": "Email sender or display name", + "add_mail_placeholder": "e.g. only4-subscribers.com or EXTRASPIN", + "add_mail_help": "Email address, domain or display name. We block all emails matching this pattern.", + "add_mail_invalid": "Please enter a pattern." }, "mail": { "title": "Mail Shield", diff --git a/apps/rebreak-native/locales/fr.json b/apps/rebreak-native/locales/fr.json index 413ab14..2346fe8 100644 --- a/apps/rebreak-native/locales/fr.json +++ b/apps/rebreak-native/locales/fr.json @@ -316,7 +316,16 @@ "cooldown_elapsed_message": "La pause de sécurité est terminée — la protection a été désactivée. Vous pouvez maintenant désactiver le service d'accessibilité ReBreak dans les Réglages.", "cooldown_elapsed_open_settings": "Ouvrir les Réglages", "app_lock_coming_soon_badge": "Bientôt", - "app_lock_coming_soon_desc": "Verrouillage de l'app bientôt disponible — la protection par filtre est déjà active." + "app_lock_coming_soon_desc": "Verrouillage de l'app bientôt disponible — la protection par filtre est déjà active.", + "type_web": "Site web", + "type_mail": "E-mail", + "add_web_label": "Domaine", + "add_web_placeholder": "ex. casino.com", + "add_web_help": "Ce site sera bloqué sur tous vos appareils protégés.", + "add_mail_label": "Expéditeur ou nom affiché", + "add_mail_placeholder": "ex. only4-subscribers.com ou EXTRASPIN", + "add_mail_help": "Adresse e-mail, domaine ou nom affiché. Nous bloquons tous les mails correspondant à ce modèle.", + "add_mail_invalid": "Veuillez saisir un modèle." }, "mail": { "title": "Protection Mail",