diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx
index 559fa84..851d01e 100644
--- a/apps/rebreak-native/app/(app)/blocker.tsx
+++ b/apps/rebreak-native/app/(app)/blocker.tsx
@@ -1,9 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react';
-import { ScrollView, Text, View, Alert, ActivityIndicator } from 'react-native';
+import { 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';
@@ -42,6 +43,8 @@ export default function BlockerScreen() {
const {
domains,
tier,
+ countsByType,
+ limits,
addDomain,
submitDomain,
refresh: refreshDomains,
@@ -62,6 +65,9 @@ export default function BlockerScreen() {
}, [refreshDomains, syncBlocklist, refresh]);
useDomainSubmissionRealtime(onDomainChange, true);
+ // Tab-State
+ const [activeTab, setActiveTab] = useState<'web' | 'mail'>('web');
+
// Sheet-States
const [addSheetOpen, setAddSheetOpen] = useState(false);
const [detailsOpen, setDetailsOpen] = useState(false);
@@ -367,11 +373,20 @@ export default function BlockerScreen() {
)}
+ {/* Top-Tabs: Seiten / Mails */}
+
+
{/* Domain Grid mit inline + Button neben SlotPill */}
setAddSheetOpen(true)}
onSubmit={submitDomain}
onUpgradePro={() => Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))}
@@ -383,6 +398,7 @@ export default function BlockerScreen() {
{
setAddSheetOpen(false);
refreshDomains();
@@ -417,3 +433,88 @@ export default function BlockerScreen() {
);
}
+
+// ─── BlockerTabBar ────────────────────────────────────────────────────────────
+
+function BlockerTabBar({
+ activeTab,
+ onTabChange,
+ countsByType,
+ limits,
+}: {
+ activeTab: 'web' | 'mail';
+ onTabChange: (tab: 'web' | 'mail') => void;
+ countsByType: CountsByType;
+ limits: LimitsByType;
+}) {
+ 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 },
+ ];
+
+ return (
+
+ {tabs.map((tab) => {
+ const isActive = activeTab === tab.key;
+ const atMax = tab.count >= tab.max;
+ const badgeColor = atMax ? colors.error : colors.textMuted;
+
+ return (
+ onTabChange(tab.key)}
+ activeOpacity={0.7}
+ style={{
+ flex: 1,
+ alignItems: 'center',
+ paddingBottom: 10,
+ paddingTop: 4,
+ borderBottomWidth: 2,
+ borderBottomColor: isActive ? colors.brandOrange : 'transparent',
+ }}
+ >
+
+
+ {tab.label}
+
+
+
+ {t('blocker.count_label', { count: tab.count, max: tab.max })}
+
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/rebreak-native/components/blocker/DomainGrid.tsx b/apps/rebreak-native/components/blocker/DomainGrid.tsx
index 2b8a660..b6d3797 100644
--- a/apps/rebreak-native/components/blocker/DomainGrid.tsx
+++ b/apps/rebreak-native/components/blocker/DomainGrid.tsx
@@ -2,7 +2,6 @@ import { useState, useMemo } from 'react';
import {
View,
Text,
- Pressable,
TouchableOpacity,
Image,
ActivityIndicator,
@@ -50,6 +49,7 @@ function timeSinceSubmit(input?: string | Date): string {
type Props = {
domains: CustomDomain[];
tier: Tier;
+ activeTab: 'web' | 'mail';
onAdd?: () => void;
onSubmit?: (id: string) => Promise<{ ok: boolean }>;
onRemove?: (id: string) => Promise<{ ok: boolean }>;
@@ -65,14 +65,19 @@ const STATUS_PRIORITY: Record = {
approved: 99,
};
-export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) {
+export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgradePro }: Props) {
const { t } = useTranslation();
const colors = useColors();
- // Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority,
- // innerhalb gleicher Priority dann newest-first by addedAt.
+ // Filter by tab, then by status (exclude approved). Sort by status-priority, then newest-first.
const visible = useMemo(() => {
return domains
- .filter((d) => d.status !== 'approved')
+ .filter((d) => {
+ if (d.status === 'approved') return false;
+ if (activeTab === 'mail') {
+ return d.type === 'mail_domain' || d.type === 'mail_display_name';
+ }
+ return d.type === 'web' || !d.type;
+ })
.slice()
.sort((a, b) => {
const pa = STATUS_PRIORITY[a.status] ?? 99;
@@ -82,7 +87,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
const tb = b.addedAt ? new Date(b.addedAt).getTime() : 0;
return tb - ta;
});
- }, [domains]);
+ }, [domains, activeTab]);
return (
@@ -94,11 +99,11 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
{onAdd && (
-
-
+
)}
@@ -179,7 +183,11 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
alignItems: 'center',
}}
>
-
+
- {t('blocker.domain_empty')}
+ {activeTab === 'mail' ? t('blocker.empty_mail') : t('blocker.empty_web')}
) : (
@@ -341,9 +349,10 @@ function DomainTile({
}
}
+ const isMailDisplayName = domain.type === 'mail_display_name';
const isFreeAndUsed = tier.plan === 'free' && domain.status !== 'active';
- const showSubmit = tier.canSubmit && domain.status === 'active';
- const showResubmit = tier.canSubmit && domain.status === 'rejected';
+ const showSubmit = tier.canSubmit && domain.status === 'active' && !isMailDisplayName;
+ const showResubmit = tier.canSubmit && domain.status === 'rejected' && !isMailDisplayName;
const showInPruefungBtn = domain.status === 'submitted';
return (
@@ -461,9 +470,10 @@ function DomainTile({
)}
{showSubmit && (
-
)}
-
+
)}
{showResubmit && (
-
)}
-
+
)}
diff --git a/apps/rebreak-native/hooks/useCustomDomains.ts b/apps/rebreak-native/hooks/useCustomDomains.ts
index 918f7d2..c1661c3 100644
--- a/apps/rebreak-native/hooks/useCustomDomains.ts
+++ b/apps/rebreak-native/hooks/useCustomDomains.ts
@@ -42,9 +42,21 @@ function deriveTier(plan: Plan, domains: CustomDomain[]): Tier {
};
}
+export type CountsByType = {
+ web: number;
+ mail: number;
+};
+
+export type LimitsByType = {
+ web: number;
+ mail: number;
+};
+
export type UseCustomDomainsReturn = {
domains: CustomDomain[];
tier: Tier;
+ countsByType: CountsByType;
+ limits: LimitsByType;
loading: boolean;
error: string | null;
refresh: () => Promise;
@@ -165,9 +177,24 @@ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn {
const tier = deriveTier(plan, domains);
+ const countsByType: CountsByType = {
+ web: domains.filter(
+ (d) => 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'),
+ ).length,
+ };
+
+ const webLimit = plan === 'legend' ? 8 : 4;
+ const mailLimit = plan === 'legend' ? 2 : 1;
+ const limits: LimitsByType = { web: webLimit, mail: mailLimit };
+
return {
domains,
tier,
+ countsByType,
+ limits,
loading,
error,
refresh: fetchDomains,
diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json
index c60a72d..1786eea 100644
--- a/apps/rebreak-native/locales/de.json
+++ b/apps/rebreak-native/locales/de.json
@@ -325,7 +325,14 @@
"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."
+ "add_mail_invalid": "Bitte ein Muster eingeben.",
+ "tabs_web": "Seiten",
+ "tabs_mail": "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."
},
"mail": {
"title": "Mail-Schutz",
diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json
index dac6f71..8b90e39 100644
--- a/apps/rebreak-native/locales/en.json
+++ b/apps/rebreak-native/locales/en.json
@@ -325,7 +325,14 @@
"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."
+ "add_mail_invalid": "Please enter a pattern.",
+ "tabs_web": "Websites",
+ "tabs_mail": "Emails",
+ "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)."
},
"mail": {
"title": "Mail Shield",
diff --git a/apps/rebreak-native/locales/fr.json b/apps/rebreak-native/locales/fr.json
index 2346fe8..a0cc9ab 100644
--- a/apps/rebreak-native/locales/fr.json
+++ b/apps/rebreak-native/locales/fr.json
@@ -325,7 +325,14 @@
"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."
+ "add_mail_invalid": "Veuillez saisir un modèle.",
+ "tabs_web": "Sites",
+ "tabs_mail": "E-mails",
+ "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)."
},
"mail": {
"title": "Protection Mail",