diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx
index 851d01e..9cab1a1 100644
--- a/apps/rebreak-native/app/(app)/blocker.tsx
+++ b/apps/rebreak-native/app/(app)/blocker.tsx
@@ -1,10 +1,9 @@
import { useCallback, useEffect, useRef, useState } from 'react';
-import { ScrollView, Text, View, Alert, ActivityIndicator, TouchableOpacity } from 'react-native';
+import { Animated, ScrollView, Text, View, Alert, ActivityIndicator, TouchableOpacity } from 'react-native';
import { useRouter } from 'expo-router';
import { useBottomTabBarHeight } from 'react-native-bottom-tabs';
import { useTranslation } from 'react-i18next';
import { Ionicons } from '@expo/vector-icons';
-import type { CountsByType, LimitsByType } from '../../hooks/useCustomDomains';
import { AppHeader } from '../../components/AppHeader';
import { LayerSwitchCard } from '../../components/blocker/LayerSwitchCard';
import { ProtectionLockedCard } from '../../components/blocker/ProtectionLockedCard';
@@ -52,9 +51,6 @@ export default function BlockerScreen() {
const { sync: syncBlocklist } = useBlocklistSync();
// Realtime: Domain-Submission-Status (approved/rejected/in_review) live patchen.
- // Bei domain_rejected wird die Row backend-seitig hard-deleted → refetch
- // entfernt sie aus der Liste. Zusätzlich blocklist.bin neu syncen damit
- // die lokale Hash-Liste nicht aus dem Tritt gerät.
const onDomainChange = useCallback(async () => {
await refreshDomains();
if (urlFilterActiveRef.current) {
@@ -65,28 +61,26 @@ export default function BlockerScreen() {
}, [refreshDomains, syncBlocklist, refresh]);
useDomainSubmissionRealtime(onDomainChange, true);
- // Tab-State
- const [activeTab, setActiveTab] = useState<'web' | 'mail'>('web');
+ // Section collapse state — beide Sections starten offen
+ const [webOpen, setWebOpen] = useState(true);
+ const [mailOpen, setMailOpen] = useState(true);
- // Sheet-States
+ // AddSheet state: tracks which section opened it
const [addSheetOpen, setAddSheetOpen] = useState(false);
+ const [addSheetKind, setAddSheetKind] = useState<'web' | 'mail'>('web');
+
const [detailsOpen, setDetailsOpen] = useState(false);
const [explainerOpen, setExplainerOpen] = useState(false);
- // Layer-Status (auf iOS): urlFilter + familyControls.
- // AppDeletionLock=true bedeutet "locked in" → keine Switches mehr, nur Cooldown-Pfad.
const urlFilterActive = state?.layers.urlFilter === true;
const familyControlsActive = state?.layers.familyControls === true;
const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true;
const lockedIn = appDeletionLockActive;
- // Ref damit onDomainChange nicht neu rendert bei jedem urlFilter-Toggle
const urlFilterActiveRef = useRef(urlFilterActive);
useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]);
- // Auto-Sync wenn URL-Filter beim Page-Mount/-Resume schon aktiv ist und
- // blocklist.bin leer/stale sein könnte. Dedupe via Ref damit wir nicht
- // bei jedem Re-Render neu syncen.
+ // Auto-Sync wenn URL-Filter beim Page-Mount/-Resume schon aktiv ist.
const syncedOnceRef = useRef(false);
useEffect(() => {
if (!urlFilterActive) return;
@@ -94,7 +88,7 @@ export default function BlockerScreen() {
syncedOnceRef.current = true;
syncBlocklist().then((res) => {
console.log('[blocker] auto-sync on mount:', res);
- if (res.ok) refresh(); // Stats-Card neu rendern mit aktuellem Count
+ if (res.ok) refresh();
});
}, [urlFilterActive, syncBlocklist, refresh]);
@@ -114,12 +108,9 @@ export default function BlockerScreen() {
],
);
} else {
- // Filter ist aktiv aber blocklist.bin ist initial leer — sofort syncen!
- // Sonst zeigt iOS "Läuft" aber blockt nichts.
const sync = await syncBlocklist();
console.log('[blocker] post-activate sync:', sync);
if (sync.ok) {
- // Stats-Card neu rendern mit dem frisch geschriebenen Count
await refresh();
} else {
Alert.alert(
@@ -140,10 +131,8 @@ export default function BlockerScreen() {
try {
const result = await activateFamilyControls();
console.log('[blocker] activateFamilyControls:', result);
- // `accessibility_pending` = die a11y-Berechtigung fehlt noch und wir haben
- // grad die System-Settings geöffnet → das IST das Feedback. Kein Fehler-
- // Modal (sonst Modal-Loop bei jedem Tap). a11y wird nur beim ersten
- // Einrichten geholt; danach ist das hier ein 1-Tap-Arm ohne Dialog.
+ // `accessibility_pending` = a11y-Berechtigung fehlt noch → System-Settings wurden
+ // geöffnet. Kein Fehler-Modal (sonst Modal-Loop bei jedem Tap).
if (!result.enabled && result.error !== 'accessibility_pending') {
Alert.alert(
t('blocker.activate_app_lock_failed_title'),
@@ -199,8 +188,6 @@ export default function BlockerScreen() {
}
if (bypassAlertShownRef.current) return;
bypassAlertShownRef.current = true;
- // Schutz-Filter ist aus, sollte aber an sein → Reaktivierung setzt NUR den
- // VPN/Filter wieder (kein a11y-Prompt — das passiert nur beim ersten Mal).
Alert.alert(
t('blocker.protection_off_title'),
t('blocker.protection_off_message'),
@@ -211,6 +198,11 @@ export default function BlockerScreen() {
);
}, [state?.phase, t]);
+ function openAddSheet(kind: 'web' | 'mail') {
+ setAddSheetKind(kind);
+ setAddSheetOpen(true);
+ }
+
// ─── Render ──────────────────────────────────────────────────────────
return (
@@ -226,7 +218,7 @@ export default function BlockerScreen() {
) : (
- // FC nicht aktiv → User kann pro Layer einzeln togglen
)}
- {/* CooldownBanner — nur wenn Cooldown läuft */}
+ {/* CooldownBanner */}
{state.cooldown.active && (
)}
- {/* Über-Limit: Custom-Domain-Banner */}
+ {/* Über-Limit Banner */}
{tier.atLimit && tier.usedSlots > tier.domainLimit && (
)}
- {/* Top-Tabs: Seiten / Mails */}
-
-
- {/* Domain Grid mit inline + Button neben SlotPill */}
-
+ {/* Section 1: Eigene Domains */}
+ setWebOpen((v) => !v)}
+ onAdd={() => openAddSheet('web')}
+ atLimit={countsByType.web >= limits.web}
+ >
setAddSheetOpen(true)}
+ kind="web"
onSubmit={submitDomain}
onUpgradePro={() => Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))}
/>
-
+
+
+ {/* Section 2: Eigene Mails */}
+ setMailOpen((v) => !v)}
+ onAdd={() => openAddSheet('mail')}
+ atLimit={countsByType.mail >= limits.mail}
+ >
+ Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))}
+ />
+
{/* Sheets */}
{
setAddSheetOpen(false);
refreshDomains();
@@ -406,9 +415,8 @@ export default function BlockerScreen() {
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();
- if (sync.ok) refresh(); // Stats-Card mit neuem Count refreshen
+ if (sync.ok) refresh();
}
return result;
}}
@@ -434,87 +442,152 @@ export default function BlockerScreen() {
);
}
-// ─── BlockerTabBar ────────────────────────────────────────────────────────────
+// ─── DomainSection ────────────────────────────────────────────────────────────
-function BlockerTabBar({
- activeTab,
- onTabChange,
- countsByType,
- limits,
+function DomainSection({
+ title,
+ count,
+ max,
+ open,
+ onToggle,
+ onAdd,
+ atLimit,
+ children,
}: {
- activeTab: 'web' | 'mail';
- onTabChange: (tab: 'web' | 'mail') => void;
- countsByType: CountsByType;
- limits: LimitsByType;
+ title: string;
+ count: number;
+ max: number;
+ open: boolean;
+ onToggle: () => void;
+ onAdd: () => void;
+ atLimit: boolean;
+ children: React.ReactNode;
}) {
const { t } = useTranslation();
const colors = useColors();
- const tabs: { key: 'web' | 'mail'; label: string; count: number; max: number }[] = [
- { key: 'web', label: t('blocker.tabs_web'), count: countsByType.web, max: limits.web },
- { key: 'mail', label: t('blocker.tabs_mail'), count: countsByType.mail, max: limits.mail },
- ];
+ // Animated progress bar
+ const fillAnim = useRef(new Animated.Value(0)).current;
+ const ratio = max > 0 ? Math.min(count / max, 1) : 0;
+
+ useEffect(() => {
+ Animated.timing(fillAnim, {
+ toValue: ratio,
+ duration: 380,
+ useNativeDriver: false,
+ }).start();
+ }, [ratio]);
+
+ const pct = ratio * 100;
+ const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a';
+
+ const badgeBg = atLimit ? '#fee2e2' : colors.surfaceElevated;
+ const badgeFg = atLimit ? '#dc2626' : colors.textMuted;
return (
- {tabs.map((tab) => {
- const isActive = activeTab === tab.key;
- const atMax = tab.count >= tab.max;
- const badgeColor = atMax ? colors.error : colors.textMuted;
+ {/* Section Header */}
+
+
+ {title}
+
+
+
+ {t('blocker.count_label', { count, max })}
+
+
+
+
- return (
- onTabChange(tab.key)}
- activeOpacity={0.7}
+ {open && (
+
+ {/* Progressbar */}
+
-
+
+
+
+ {/* Add-Button */}
+
+
+
- {tab.label}
+ {t('blocker.add_domain')}
-
-
- {t('blocker.count_label', { count: tab.count, max: tab.max })}
-
-
- );
- })}
+
+ {/* Grid */}
+ {children}
+
+ )}
);
}
diff --git a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx
index 9e8c2ad..ec3489b 100644
--- a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx
+++ b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx
@@ -2,7 +2,9 @@ import { useState, useEffect } from 'react';
import {
ActivityIndicator,
Image,
+ ScrollView,
Text,
+ TextInput,
TouchableOpacity,
View,
} from 'react-native';
@@ -15,7 +17,6 @@ import {
} from '../../hooks/useCustomDomains';
import { useColors, type ColorScheme } from '../../lib/theme';
import { FormSheet } from '../FormSheet';
-import { SheetFieldStack } from '../SheetFieldStack';
type InputKind = 'web' | 'mail';
@@ -35,7 +36,6 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P
const [confirmPermanent, setConfirmPermanent] = useState(false);
const [adding, setAdding] = useState(false);
const [error, setError] = useState(null);
- const [fieldsDone, setFieldsDone] = useState(false);
useEffect(() => {
if (visible) setKind(initialType ?? 'web');
@@ -44,17 +44,13 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P
const normalizedWeb = kind === 'web' ? normalizeDomain(input) : '';
// For mail input: if the user typed a full address (local@domain.tld), strip
- // the local-part and keep only the domain — casino affiliates rotate the
- // local-part rapidly (communications@, newsletter@, info@, …) while the
- // sender-domain stays stable. Blocking on the domain is more durable.
- // A bare token without "@" stays as-is (will be matched as display-name on
- // the backend).
+ // the local-part and keep only the domain. A bare domain without "@" stays as-is.
const mailPattern = (() => {
if (kind !== 'mail') return '';
const raw = input.trim();
if (!raw) return '';
const atIdx = raw.lastIndexOf('@');
- if (atIdx === -1) return raw;
+ if (atIdx === -1) return raw.toLowerCase();
return raw.slice(atIdx + 1).trim().toLowerCase();
})();
@@ -62,7 +58,6 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P
setInput('');
setConfirmPermanent(false);
setError(null);
- setFieldsDone(false);
onClose();
}
@@ -71,7 +66,6 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P
setKind(next);
setInput('');
setError(null);
- setFieldsDone(false);
}
function isInputValid(): boolean {
@@ -83,7 +77,7 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P
if (!isInputValid() || !confirmPermanent || adding) return;
setAdding(true);
setError(null);
- const pattern = kind === 'web' ? input : mailPattern;
+ const pattern = kind === 'web' ? normalizeDomain(input) : mailPattern;
const result = await onAdd(pattern, kind);
setAdding(false);
if (result.ok) {
@@ -118,39 +112,56 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P
? 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');
+ const canSubmit = isInputValid() && confirmPermanent && !adding;
return (
-
- }
- fields={[
- {
- key: 'pattern',
- label: inputLabel,
- placeholder: inputPlaceholder,
- value: input,
- onChangeText: (v) => { setInput(v); setError(null); },
- normalize: kind === 'web' ? normalizeDomain : undefined,
- keyboardType: kind === 'web' ? 'url' : 'default',
- autoCapitalize: 'none',
- autoCorrect: false,
- validate: validateField,
- },
- ]}
- onComplete={() => setFieldsDone(true)}
+
- {/* Help-Text */}
+ {/* 1. Type-Picker Pill */}
+
+
+ {/* 2. Input-Field */}
+
+
+ {inputLabel}
+
+ { setInput(v); setError(null); }}
+ placeholder={inputPlaceholder}
+ placeholderTextColor={colors.textMuted}
+ keyboardType={kind === 'web' ? 'url' : 'email-address'}
+ autoCapitalize="none"
+ autoCorrect={false}
+ style={{
+ backgroundColor: colors.surfaceElevated,
+ borderRadius: 10,
+ padding: 12,
+ fontSize: 14,
+ fontFamily: 'Nunito_400Regular',
+ color: colors.text,
+ borderWidth: 1,
+ borderColor: error ? '#dc2626' : colors.border,
+ }}
+ />
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {/* 3. Help-Text */}
- {/* Favicon-Preview (nur Web) */}
- {kind === 'web' && (
+ {/* 4. Preview-Card */}
+ {kind === 'web' ? (
- {normalizedWeb}
+ {normalizedWeb || inputPlaceholder}
- )}
-
- {/* Mail-Typ Icon-Preview */}
- {kind === 'mail' && (
+ ) : (
@@ -252,7 +255,7 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P
)}
- {/* Warnung */}
+ {/* 5. Warning-Card */}
-
+
- {/* Confirm-Checkbox */}
+ {/* 6. Confirm-Checkbox */}
setConfirmPermanent((v) => !v)}
activeOpacity={0.7}
@@ -288,7 +290,6 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P
alignItems: 'flex-start',
gap: 10,
paddingVertical: 4,
- marginBottom: 14,
}}
>
- {error && (
-
+
- {error}
-
- )}
-
- {/* Add-Button */}
-
-
- {adding ? (
-
- ) : (
-
- {t('blocker.add_sheet_title')}
+
+
+ {t('common.cancel')}
- )}
-
-
-
+
+
+
+
+
+ {adding ? (
+
+ ) : (
+
+ {t('blocker.add_sheet_cta')}
+
+ )}
+
+
+
+
);
}
// ─── TypePicker ──────────────────────────────────────────────────────────────
-function TypePicker({ kind, onChange }: { kind: InputKind; onChange: (k: InputKind) => void }) {
+function TypePicker({
+ kind,
+ onChange,
+ colors,
+}: {
+ kind: InputKind;
+ onChange: (k: InputKind) => void;
+ colors: ColorScheme;
+}) {
const { t } = useTranslation();
- const colors = useColors();
return (
void;
+ kind: 'web' | 'mail';
onSubmit?: (id: string) => Promise<{ ok: boolean }>;
- onRemove?: (id: string) => Promise<{ ok: boolean }>;
onUpgradePro?: () => void;
};
@@ -65,16 +49,16 @@ const STATUS_PRIORITY: Record = {
approved: 99,
};
-export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgradePro }: Props) {
+export function DomainGrid({ domains, tier, kind, onSubmit, onUpgradePro }: Props) {
const { t } = useTranslation();
const colors = useColors();
- // Filter by tab, then by status (exclude approved). Sort by status-priority, then newest-first.
+
const visible = useMemo(() => {
return domains
.filter((d) => {
if (d.status === 'approved') return false;
- if (activeTab === 'mail') {
- return d.type === 'mail_domain' || d.type === 'mail_display_name';
+ if (kind === 'mail') {
+ return d.type === 'mail_domain';
}
return d.type === 'web' || !d.type;
})
@@ -87,60 +71,10 @@ export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgrad
const tb = b.addedAt ? new Date(b.addedAt).getTime() : 0;
return tb - ta;
});
- }, [domains, activeTab]);
+ }, [domains, kind]);
return (
- {/* Header: Section-Title + Slot-Counter + Add-Button (inline, neben SlotPill) */}
-
-
- {t('blocker.domain_section_title')}
-
-
-
- {onAdd && (
-
-
-
-
-
- )}
-
-
-
- {/* Progress-Bar — 3-stufige Color-Schwelle: <60% grün, 60-90% orange, >=90% rot */}
- {(() => {
- const pct = (tier.usedSlots / tier.domainLimit) * 100;
- const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a';
- return (
-
-
-
- );
- })()}
-
{/* Limit-Reached Upsell (nur Free) */}
{tier.atLimit && tier.plan === 'free' && (
@@ -197,7 +131,7 @@ export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgrad
textAlign: 'center',
}}
>
- {activeTab === 'mail' ? t('blocker.empty_mail') : t('blocker.empty_web')}
+ {kind === 'mail' ? t('blocker.empty_mail') : t('blocker.empty_web')}
) : (
@@ -211,28 +145,6 @@ export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgrad
);
}
-// ─── SlotPill ─────────────────────────────────────────────────────────────
-
-function SlotPill({ tier }: { tier: Tier }) {
- const colors = useColors();
- const bg = tier.atLimit ? '#fee2e2' : colors.surfaceElevated;
- const fg = tier.atLimit ? '#dc2626' : colors.textMuted;
- return (
-
-
- {tier.usedSlots}/{tier.domainLimit}
-
-
- );
-}
-
// ─── Tiles ────────────────────────────────────────────────────────────────
function DomainTilesGrid({
@@ -245,7 +157,7 @@ function DomainTilesGrid({
onSubmit?: (id: string) => Promise<{ ok: boolean }>;
}) {
// 3-Spalten-Grid via flex-wrap. Parent ScrollView (in blocker.tsx) handles scroll —
- // KEIN nested ScrollView hier, sonst collabiert der Layout-Pass weil ScrollView
+ // KEIN nested ScrollView hier, sonst kollabiert der Layout-Pass weil ScrollView
// inner-content-view keine definierte Width für %-basierte Tile-Widths hat.
return (
@@ -281,21 +193,17 @@ function DomainTile({
const isLegend = tier.plan === 'legend';
- // statusColor wird auf Badge + Button angewendet.
- // iOS-native: blue (active), orange (submitted), red (rejected).
const statusColor = (() => {
switch (domain.status) {
case 'submitted':
- return '#f59e0b'; // orange (Voting/Prüfung)
+ return '#f59e0b';
case 'rejected':
- return '#FF3B30'; // iOS-red
+ return '#FF3B30';
default:
- return '#007AFF'; // iOS-blue (active, "freigeben"-CTA)
+ return '#007AFF';
}
})();
- // Time-Color: nur Status die Aufmerksamkeit brauchen (submitted/rejected) sind farbig.
- // Active = neutral gray (settled state, kein Alarm-Indikator nötig).
const timeColor = domain.status === 'active' ? '#a3a3a3' : statusColor;
const badgeLabel = (() => {
@@ -309,8 +217,6 @@ function DomainTile({
}
})();
- // Tier-aware Confirm-Dialog vor Freigabe — Pro geht zu Community-Voting,
- // Legend direkt zum ReBreak-Team. Animiertes Modal statt nativem Alert.
const isResubmit = domain.status === 'rejected';
const confirmTitle = isLegend
? isResubmit
@@ -363,15 +269,12 @@ function DomainTile({
borderColor: colors.border,
borderRadius: 14,
padding: 8,
- // KEIN aspectRatio:1 mehr — der hat den Button auf 0 Höhe gepresst.
- // minHeight statt fixer aspect-ratio: Tile darf wachsen wenn Button da ist,
- // bleibt aber konsistent groß für visuelle Stabilität.
minHeight: 130,
opacity: isFreeAndUsed ? 0.55 : 1,
gap: 6,
}}
>
- {/* Top-Row: Zeit links · Badge rechts — beide in Status-Color (matcht Bottom-Button). */}
+ {/* Top-Row: Zeit links · Badge rechts */}
- {/* Mitte: Icon + Domain-Name (zentriert, flex-1) */}
+ {/* Mitte: Icon + Domain-Name */}
{domain.type === 'mail_domain' || domain.type === 'mail_display_name' ? (
- {/* Bottom-Slot: ALWAYS rendered Container (32px), Inhalt je nach Status.
- * Garantiert konsistente Tile-Höhe + sichtbaren Button. */}
+ {/* Bottom-Slot: ALWAYS rendered Container (28px), Inhalt je nach Status. */}
{showInPruefungBtn && (
- {/* Confirm-Modal vor Submit (statt nativer Alert.alert — selber animation-Style wie SuccessAlert) */}
setConfirmVisible(false)}
/>
- {/* Success-Alert mit animiertem Check-Icon nach erfolgreichem Submit */}
d.status !== 'approved' && (d.type === 'web' || !d.type),
).length,
mail: domains.filter(
- (d) => d.status !== 'approved' && (d.type === 'mail_domain' || d.type === 'mail_display_name'),
+ (d) => d.status !== 'approved' && d.type === 'mail_domain',
).length,
};
diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json
index 1786eea..ce2c6f0 100644
--- a/apps/rebreak-native/locales/de.json
+++ b/apps/rebreak-native/locales/de.json
@@ -322,17 +322,20 @@
"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_label": "E-Mail-Adresse oder Domain",
+ "add_mail_placeholder": "z.B. newsletter@casino.com oder casino.com",
+ "add_mail_help": "E-Mail-Adresse oder Mail-Domain. Wir blockieren alle Mails von diesem Absender.",
"add_mail_invalid": "Bitte ein Muster eingeben.",
+ "add_sheet_cta": "Hinzufügen",
"tabs_web": "Seiten",
"tabs_mail": "Mails",
+ "section_domains": "Eigene Domains",
+ "section_mails": "Eigene Mails",
"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.",
"empty_web": "Noch keine eigenen Domains.\nTippe + um eine hinzuzufügen.",
- "empty_mail": "Noch keine E-Mail-Patterns. Tippe + um eine Adresse oder einen Display-Namen (z.B. EXTRASPIN) zu blockieren."
+ "empty_mail": "Noch keine Mail-Domains. Tippe + um eine E-Mail-Adresse oder Domain zu blockieren."
},
"mail": {
"title": "Mail-Schutz",
diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json
index 8b90e39..0bb54eb 100644
--- a/apps/rebreak-native/locales/en.json
+++ b/apps/rebreak-native/locales/en.json
@@ -322,17 +322,20 @@
"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_label": "Email address or domain",
+ "add_mail_placeholder": "e.g. newsletter@casino.com or casino.com",
+ "add_mail_help": "Email address or mail domain. We block all emails from this sender.",
"add_mail_invalid": "Please enter a pattern.",
+ "add_sheet_cta": "Add",
"tabs_web": "Websites",
"tabs_mail": "Emails",
+ "section_domains": "Your Domains",
+ "section_mails": "Your Email Filters",
"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.",
"empty_web": "No custom domains yet.\nTap + to add one.",
- "empty_mail": "No email patterns yet. Tap + to block an address or display name (e.g. EXTRASPIN)."
+ "empty_mail": "No mail domains yet. Tap + to block an email address or domain."
},
"mail": {
"title": "Mail Shield",
diff --git a/apps/rebreak-native/locales/fr.json b/apps/rebreak-native/locales/fr.json
index a0cc9ab..02a03b4 100644
--- a/apps/rebreak-native/locales/fr.json
+++ b/apps/rebreak-native/locales/fr.json
@@ -322,17 +322,20 @@
"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_label": "Adresse e-mail ou domaine",
+ "add_mail_placeholder": "ex. newsletter@casino.com ou casino.com",
+ "add_mail_help": "Adresse e-mail ou domaine mail. Nous bloquons tous les mails de cet expéditeur.",
"add_mail_invalid": "Veuillez saisir un modèle.",
+ "add_sheet_cta": "Ajouter",
"tabs_web": "Sites",
"tabs_mail": "E-mails",
+ "section_domains": "Mes domaines",
+ "section_mails": "Mes filtres mail",
"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.",
"empty_web": "Aucun domaine personnalisé.\nAppuyez sur + pour en ajouter un.",
- "empty_mail": "Aucun filtre e-mail. Appuyez sur + pour bloquer une adresse ou un nom affiché (ex. EXTRASPIN)."
+ "empty_mail": "Aucun domaine mail. Appuyez sur + pour bloquer une adresse ou un domaine."
},
"mail": {
"title": "Protection Mail",