diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx index 69c4536..e0a61af 100644 --- a/apps/rebreak-native/app/(app)/blocker.tsx +++ b/apps/rebreak-native/app/(app)/blocker.tsx @@ -43,8 +43,8 @@ export default function BlockerScreen() { const { domains, tier, - countsByType, - limits, + count: domainCount, + limit: domainLimit, addDomain, submitDomain, refresh: refreshDomains, @@ -324,8 +324,8 @@ export default function BlockerScreen() { setAddSheetOpen(true)} onSubmitDomain={submitDomain} diff --git a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx index 2928228..3aca1bc 100644 --- a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx +++ b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx @@ -113,16 +113,8 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) { return; } 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 === 'limit_reached' || raw.includes('limit_reached')) { - setError( - kind === 'mail' - ? t('blocker.error_mail_limit_reached') - : t('blocker.error_web_limit_reached'), - ); + if (raw.includes('limit_reached')) { + setError(t('blocker.error_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')) { diff --git a/apps/rebreak-native/hooks/useCustomDomains.ts b/apps/rebreak-native/hooks/useCustomDomains.ts index d182642..f547c04 100644 --- a/apps/rebreak-native/hooks/useCustomDomains.ts +++ b/apps/rebreak-native/hooks/useCustomDomains.ts @@ -38,16 +38,17 @@ export type AddDomainResult = { export type Tier = { plan: Plan; - domainLimit: number; // free=5, pro=5, legend=10 - refillEnabled: boolean; // free=false, pro/legend=true - globalBlocklist: boolean; // free=false, pro/legend=true - canSubmit: boolean; // free=false, pro/legend=true + domainLimit: number; // pro=10, legend=20 (web + mail gemeinsam) + refillEnabled: boolean; // pro/legend=true + globalBlocklist: boolean; // pro/legend=true + canSubmit: boolean; // pro/legend=true usedSlots: number; // active+submitted (NICHT approved/rejected) atLimit: boolean; }; function deriveTier(plan: Plan, domains: CustomDomain[]): Tier { - const limit = plan === 'legend' ? 10 : 5; + // Slots: EIN gemeinsamer Pool für web + mail. Free-Tier ist entfallen. + const limit = plan === 'legend' ? 20 : 10; const refill = plan !== 'free'; const usedSlots = domains.filter((d) => d.status === 'active' || d.status === 'submitted').length; return { @@ -61,21 +62,13 @@ 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; + /** Belegte Slots (active + submitted) — web + mail gemeinsamer Pool. */ + count: number; + /** Slot-Limit gesamt (Pro 10 / Legend 20). */ + limit: number; loading: boolean; error: string | null; refresh: () => Promise; @@ -113,43 +106,42 @@ export function isValidDomain(input: string): boolean { /** * Custom-Domain CRUD gegen `/api/custom-domains/*` mit Tier-aware Limits. * - * Tier-Logik (Single-Source-of-Truth: User.plan): - * Free → 5 Slots, kein Refill, keine Submit - * Pro → 5 Slots, Refill bei approved/rejected, Submit erlaubt - * Legend → 10 Slots, Refill, Submit + * Slot-Modell (Single-Source-of-Truth: User.plan): + * Pro → 10 Slots, Refill bei approved/rejected, Submit erlaubt + * Legend → 20 Slots, Refill, Submit + * web + mail teilen sich EINEN gemeinsamen Slot-Pool. Free-Tier ist entfallen. */ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn { const [domains, setDomains] = useState([]); - const [apiCounts, setApiCounts] = useState(null); - const [apiLimits, setApiLimits] = useState(null); + const [apiCount, setApiCount] = useState(null); + const [apiLimit, setApiLimit] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchDomains = useCallback(async () => { try { // Backend `GET /api/custom-domains` returns - // { items: CustomDomain[], counts: { web, mail }, limits: { web, mail } } - // since the slot-pool split (commit f2b81ee). Legacy fall-throughs cover - // an older shape (bare array or { domains }) in case a cached client - // ever hits this code path before the deploy lands. + // { items: CustomDomain[], count: number, limit: number } + // (count/limit = gemeinsamer web+mail-Slot-Pool). Array-Fallback deckt + // eine ältere Response-Form ab, falls ein gecachter Client das hier + // trifft bevor das Deploy landet. const res = await apiFetch< | CustomDomain[] - | { domains?: CustomDomain[] } - | { items?: CustomDomain[]; counts?: CountsByType; limits?: LimitsByType } + | { items?: CustomDomain[]; domains?: CustomDomain[]; count?: number; limit?: number } >('/api/custom-domains'); let arr: CustomDomain[] = []; - let counts: CountsByType | null = null; - let limits: LimitsByType | null = null; + let count: number | null = null; + let limit: number | null = null; if (Array.isArray(res)) { arr = res; } else if (res) { arr = (res as any).items ?? (res as any).domains ?? []; - counts = (res as any).counts ?? null; - limits = (res as any).limits ?? null; + count = typeof (res as any).count === 'number' ? (res as any).count : null; + limit = typeof (res as any).limit === 'number' ? (res as any).limit : null; } setDomains(arr); - setApiCounts(counts); - setApiLimits(limits); + setApiCount(count); + setApiLimit(limit); setError(null); } catch (e: any) { console.error('[useCustomDomains] fetch failed:', e?.message ?? e); @@ -172,20 +164,12 @@ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn { const resolvedKind: 'web' | 'mail' = kind ?? (input.includes('@') ? 'mail' : 'web'); if (resolvedKind === 'web' && !isValidDomain(input)) return { ok: false, error: 'invalid_domain' }; if (resolvedKind === 'mail' && !input.trim()) return { ok: false, error: 'invalid_pattern' }; - // Per-Bucket-Limit-Check via Backend-counts/limits (Single Source of Truth). - // Wenn API noch keine counts/limits geliefert hat (Legacy-Response) → skip, - // Backend rejected dann mit WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED. + // Slot-Limit-Vorabcheck gegen den Backend-count/limit (Single Source of + // Truth — EIN gemeinsamer Pool). Wenn die API noch keine count/limit + // geliefert hat → skip, das Backend rejected dann mit LIMIT_REACHED. // Entfällt bei addToVip: 'approved'-Einträge belegen keinen Slot. - if (!opts?.addToVip && apiCounts && apiLimits) { - const bucket = resolvedKind; - const used = apiCounts[bucket] ?? 0; - const cap = apiLimits[bucket] ?? Infinity; - if (used >= cap) { - return { - ok: false, - error: bucket === 'mail' ? 'mail_limit_reached' : 'web_limit_reached', - }; - } + if (!opts?.addToVip && apiCount != null && apiLimit != null && apiCount >= apiLimit) { + return { ok: false, error: 'limit_reached' }; } const pattern = resolvedKind === 'web' ? normalizeDomain(input) : input.trim(); const body: Record = { pattern }; @@ -207,7 +191,7 @@ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn { return { ok: false, error: e?.message ?? 'add_failed' }; } }, - [apiCounts, apiLimits, fetchDomains], + [apiCount, apiLimit, fetchDomains], ); const submitDomain = useCallback( @@ -240,28 +224,18 @@ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn { const tier = deriveTier(plan, domains); - // Prefer API-driven counts/limits when the backend returned the new shape; - // fall back to local derivation so the UI works during a stale-bundle moment. - const countsByType: CountsByType = - apiCounts ?? { - web: domains.filter( - (d) => d.status !== 'approved' && (d.type === 'web' || !d.type), - ).length, - mail: domains.filter( - (d) => d.status !== 'approved' && d.type === 'mail_domain', - ).length, - }; - const limits: LimitsByType = - apiLimits ?? { - web: plan === 'legend' ? 10 : 5, - mail: plan === 'legend' ? 10 : 5, - }; + // API-Werte bevorzugen (Single Source of Truth); lokale Ableitung als + // Fallback, damit die UI auch bei einem stale-bundle-Moment funktioniert. + const count: number = + apiCount ?? + domains.filter((d) => d.status === 'active' || d.status === 'submitted').length; + const limit: number = apiLimit ?? (plan === 'legend' ? 20 : 10); return { domains, tier, - countsByType, - limits, + count, + limit, loading, error, refresh: fetchDomains, diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index 5adaa64..2e16fb7 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -376,6 +376,7 @@ "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_limit_reached": "Alle Domain-Slots belegt. Reiche eine Domain zur Freigabe ein — sobald sie aufgenommen ist, wird ein Slot frei.", "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.", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index a2c4ca0..82b80bf 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -376,6 +376,7 @@ "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_limit_reached": "All domain slots are full. Submit a domain for review — a slot frees up once it's accepted.", "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.", diff --git a/backend/server/api/coach/message.post.ts b/backend/server/api/coach/message.post.ts index cba1421..e73ea7e 100644 --- a/backend/server/api/coach/message.post.ts +++ b/backend/server/api/coach/message.post.ts @@ -253,7 +253,6 @@ import { stripMarkdown } from "../../utils/strip-markdown"; * Preise bleiben hier hardcoded — gehören eher zur Billing-Domain. */ function generatePlanDetails(): string { - const free = PLAN_LIMITS.free; const pro = PLAN_LIMITS.pro; const legend = PLAN_LIMITS.legend; const fmtCount = (n: number) => (n === Infinity ? "Unbegrenzt" : String(n)); @@ -262,25 +261,18 @@ function generatePlanDetails(): string { ? "(rückfüllbar – Slot wird wieder frei wenn die Domain global aufgenommen ODER von der Community abgelehnt wurde)" : "(NICHT rückfüllbar – einmal belegt, bleibt für immer belegt)"; - return `Free (0 €): -- Gambling-Blocker mit ${free.customDomains.web} eigenen Web-Domains + ${free.customDomains.mail} Mail-Patterns ${refillNote(free.domainRefill)} -- Free kann Custom Domains NICHT zur Community-Abstimmung einreichen — das ist Pro/Legend exklusiv -- ${fmtCount(free.mailAgents)} Mail-Konto, Scan alle ${free.mailIntervalOptions[0]}h + return `Pro (3,99 € / Monat oder 29 € / Jahr – spare 19 %): +- Gambling-Blocker mit Zugang zur vollständigen 208.000+ globalen Blocklist (Community-gepflegt) +- ${pro.customDomains} eigene Domains, frei aufteilbar auf Web + Mail ${refillNote(pro.domainRefill)} +- Bis zu ${fmtCount(pro.mailAgents)} Mail-Konten, Scan-Intervall wählbar (${pro.mailIntervalOptions.join("h / ")}h) - Streak-Tracker, SOS-Hilfe & Spiele-Sammlung, Atemübung - Community (lesen, posten, voten) -- KI-Coach (du, Lyra – Basismodell) - -Pro (3,99 € / Monat oder 29 € / Jahr – spare 19 %): -- Alles aus Free PLUS: -- Zugang zur vollständigen 208.000+ globalen Blocklist (Community-gepflegt) -- ${pro.customDomains.web} Web-Domains + ${pro.customDomains.mail} Mail-Patterns ${refillNote(pro.domainRefill)} -- Bis zu ${fmtCount(pro.mailAgents)} Mail-Konten, Scan-Intervall wählbar (${pro.mailIntervalOptions.join("h / ")}h) -- Stärkeres KI-Modell (du, Lyra wirst zu einem 70B-Modell) +- KI-Coach (du, Lyra – starkes 70B-Modell) - Kann Custom Domains zur Community-Abstimmung einreichen Legend (7,99 € / Monat oder 59 € / Jahr – spare 38 %): - Alles aus Pro PLUS: -- ${legend.customDomains.web} Web-Domains + ${legend.customDomains.mail} Mail-Patterns ${refillNote(legend.domainRefill)} +- ${legend.customDomains} eigene Domains, frei aufteilbar auf Web + Mail ${refillNote(legend.domainRefill)} - ⭐ MULTI-DEVICE-SCHUTZ: App auf bis zu 3 WEITEREN Geräten gleichzeitig — Familie, Partner, Eltern können mitgeschützt werden ohne extra zu zahlen. Real-life-relevant: viele Betroffene haben mehrere Geräte (iPhone + iPad + alter Laptop) - ⭐ MAIL-DAEMON (echter technischer Durchbruch — Alleinstellungsmerkmal!): ${fmtCount(legend.mailAgents)} Mail-Konten mit Echtzeit-IMAP-IDLE-Überwachung. Casino-Mails werden in Sekunden permanent gelöscht — sie tauchen nicht mal im Papierkorb auf. Der User sieht nichts. Kein "Sie haben gewonnen!"-Trigger erreicht je das Postfach. Keine andere App im Markt kann das. - Privilegierte Domain-Einreichung: umgeht Community-Vote komplett. Domains werden direkt + priorisiert vom ReBreak-Admin geprüft (schneller als die 24h-Standard-Prüfung der Pro-Submissions). Vertrauensvorteil als Legend. diff --git a/backend/server/api/custom-domains/index.get.ts b/backend/server/api/custom-domains/index.get.ts index 3e57204..4624238 100644 --- a/backend/server/api/custom-domains/index.get.ts +++ b/backend/server/api/custom-domains/index.get.ts @@ -1,21 +1,18 @@ -import { getUserCustomDomains, countActiveCustomDomainsSplit } from "../../db/domains"; +import { getUserCustomDomains, countActiveCustomDomains } from "../../db/domains"; import { getProfile } from "../../db/profile"; import { getPlanLimits } from "../../utils/plan-features"; export default defineEventHandler(async (event) => { const user = await requireUser(event); - const [items, split, profile] = await Promise.all([ + const [items, count, profile] = await Promise.all([ getUserCustomDomains(user.id), - countActiveCustomDomainsSplit(user.id), + countActiveCustomDomains(user.id), getProfile(user.id), ]); - const limits = getPlanLimits(profile?.plan ?? "free"); + // Slot-Pool: web + mail gemeinsam (Pro 10 / Legend 20). + const limit = getPlanLimits(profile?.plan ?? "pro").customDomains; - return { - items, - counts: split, - limits: limits.customDomains, - }; + return { items, count, limit }; }); diff --git a/backend/server/api/custom-domains/index.post.ts b/backend/server/api/custom-domains/index.post.ts index d0d0852..44673fa 100644 --- a/backend/server/api/custom-domains/index.post.ts +++ b/backend/server/api/custom-domains/index.post.ts @@ -1,7 +1,7 @@ import { awardPoints } from "../../utils/scoring"; import { addUserCustomDomain, - countActiveCustomDomainsSplit, + countActiveCustomDomains, CUSTOM_DOMAIN_TYPES, type CustomDomainType, } from "../../db/domains"; @@ -221,27 +221,22 @@ export default defineEventHandler(async (event) => { // !inGlobal → normaler Add unten } - // Per-type Slot-Limit prüfen — entfällt für webAddAsApproved (approved - // belegt keinen Slot). - const bucket: "web" | "mail" = type === "web" ? "web" : "mail"; + // Slot-Limit prüfen — EIN gemeinsamer Pool für web + mail (Pro 10 / Legend + // 20). Entfällt für webAddAsApproved (approved belegt keinen Slot). if (!webAddAsApproved) { const profile = await getProfile(user.id); - const limits = getPlanLimits(profile?.plan ?? "free"); - const bucketLimit = limits.customDomains[bucket]; + const limit = getPlanLimits(profile?.plan ?? "pro").customDomains; - if (bucketLimit !== Infinity) { - const split = await countActiveCustomDomainsSplit(user.id); - const currentCount = split[bucket]; - if (currentCount >= bucketLimit) { - const errorCode = bucket === "web" ? "WEB_LIMIT_REACHED" : "MAIL_LIMIT_REACHED"; + if (limit !== Infinity) { + const currentCount = await countActiveCustomDomains(user.id); + if (currentCount >= limit) { throw createError({ statusCode: 403, data: { - error: errorCode, + error: "LIMIT_REACHED", resource: "custom_domains", - bucket, current: currentCount, - limit: bucketLimit, + limit, }, }); } diff --git a/backend/server/api/plan/change-preview.get.ts b/backend/server/api/plan/change-preview.get.ts index 6044765..bb82376 100644 --- a/backend/server/api/plan/change-preview.get.ts +++ b/backend/server/api/plan/change-preview.get.ts @@ -144,14 +144,12 @@ export default defineEventHandler(async (event): Promise } // ── Custom Domains ──────────────────────────────────────────────────────── - // Compare total (web + mail) to detect any bucket change - const fromTotalDomains = fromLimits.customDomains.web + fromLimits.customDomains.mail; - const toTotalDomains = toLimits.customDomains.web + toLimits.customDomains.mail; + // Gemeinsamer Pool für web + mail (Pro 10 / Legend 20). + const fromTotalDomains = fromLimits.customDomains; + const toTotalDomains = toLimits.customDomains; if (fromTotalDomains !== toTotalDomains) { if (direction === "downgrade") { - const newLimitWeb = toLimits.customDomains.web; - const newLimitMail = toLimits.customDomains.mail; - const newLimitTotal = newLimitWeb + newLimitMail; + const newLimitTotal = toLimits.customDomains; const overBy = Math.max(0, activeDomainCount - newLimitTotal); changes.push({ resource: "custom_domains", @@ -161,15 +159,13 @@ export default defineEventHandler(async (event): Promise action: "keep", // grandfathered — alle bleiben aktiv detail: overBy > 0 - ? `Du hast ${activeDomainCount} eigene Domains, ${to}-Plan erlaubt ${newLimitTotal} (${newLimitWeb} Web + ${newLimitMail} Mail). ` + - `Alle bleiben aktiv — du kannst erst wieder welche hinzufügen wenn du unter dem jeweiligen Limit bist.` - : `Du hast ${activeDomainCount} von ${newLimitTotal} möglichen Domains (${newLimitWeb} Web + ${newLimitMail} Mail) — kein Überlauf.`, + ? `Du hast ${activeDomainCount} eigene Domains, ${to}-Plan erlaubt ${newLimitTotal}. ` + + `Alle bleiben aktiv — du kannst erst wieder welche hinzufügen wenn du unter dem Limit bist.` + : `Du hast ${activeDomainCount} von ${newLimitTotal} möglichen Domains — kein Überlauf.`, }); } else { - const web = toLimits.customDomains.web; - const mail = toLimits.customDomains.mail; gains.push( - `Bis zu ${web} Web-Domains + ${mail} Mail-Patterns${toLimits.domainRefill ? " (Slots füllen sich auf wenn deine Domain in die globale Liste aufgenommen wird)" : ""}`, + `Bis zu ${toLimits.customDomains} eigene Domains, Web oder Mail${toLimits.domainRefill ? " (Slots füllen sich auf wenn deine Domain in die globale Liste aufgenommen wird)" : ""}`, ); } } diff --git a/backend/server/db/domains.ts b/backend/server/db/domains.ts index 16309ec..5bc72c1 100644 --- a/backend/server/db/domains.ts +++ b/backend/server/db/domains.ts @@ -74,11 +74,12 @@ export async function getUserCustomDomains(userId: string) { } /** - * Counts domains that occupy a slot (active + submitted). - * approved → slot freed (domain joined global list) - * rejected → slot freed (user can re-submit or delete) + * Zählt die Domains, die einen Slot belegen (active + submitted). + * approved → Slot frei (Domain in globale Liste aufgenommen) + * rejected → Slot frei (User kann neu einreichen) * - * @deprecated Use countActiveCustomDomainsSplit for per-type quota checks. + * Single Source of Truth für das Slot-Limit — web + mail teilen sich EINEN + * gemeinsamen Pool (Pro 10 / Legend 20). */ export async function countActiveCustomDomains(userId: string) { const db = usePrisma(); diff --git a/backend/server/utils/plan-features.ts b/backend/server/utils/plan-features.ts index b0d0cee..855ee3b 100644 --- a/backend/server/utils/plan-features.ts +++ b/backend/server/utils/plan-features.ts @@ -13,17 +13,10 @@ export interface VoiceConfig { dailyQuotaSeconds: number; } -export interface CustomDomainLimits { - /** Max. Web-Domain-Slots (Infinity = unbegrenzt) */ - web: number; - /** Max. Mail-Slots — kombiniert für mail_domain + mail_display_name */ - mail: number; -} - export interface PlanLimits { // ─── Custom Domains ────────────────────────────────────────────────────── - /** Max. eigene Domains aufgeteilt nach Typ-Bucket */ - customDomains: CustomDomainLimits; + /** Max. eigene Domains — EIN gemeinsamer Pool für web + mail (Infinity = unbegrenzt) */ + customDomains: number; /** Freigeschaltete Domain-Slots füllen sich wieder auf (Community-Promotion) */ domainRefill: boolean; @@ -84,33 +77,10 @@ export interface PlanLimits { voice: VoiceConfig; } -export const PLAN_LIMITS: Record = { - free: { - customDomains: { web: 5, mail: 5 }, - domainRefill: false, - mailAgents: 1, - mailIntervalOptions: [4], - globalBlocklist: "curated", - canPost: true, - canCreateGroup: false, - canAddToBlocklist: false, - maxAppDevices: 1, - maxProtectedDevices: 0, - aiModel: "llama-3.1-8b-instant", - aiModelFallbacks: [ - { provider: "groq", model: "llama-3.3-70b-versatile" }, - { provider: "groq", model: "gemma2-9b-it" }, - { provider: "openrouter", model: "meta-llama/llama-3.1-8b-instruct" }, - ], - aiProvider: "groq", - voice: { - provider: "google", - model: "de-DE-Neural2-F", // Google Cloud TTS Neural2 — natural, ~$4/1M chars - dailyQuotaSeconds: 60, // 1 Minute/Tag - }, - }, +// Free-Tier ist entfallen — es gibt nur noch Pro + Legend. +export const PLAN_LIMITS: Record, PlanLimits> = { pro: { - customDomains: { web: 5, mail: 5 }, + customDomains: 10, domainRefill: true, mailAgents: 3, mailIntervalOptions: [1, 4, 8], @@ -133,7 +103,7 @@ export const PLAN_LIMITS: Record = { }, }, legend: { - customDomains: { web: 10, mail: 10 }, + customDomains: 20, domainRefill: true, mailAgents: Infinity, mailIntervalOptions: [1, 4, 8], @@ -158,10 +128,10 @@ export const PLAN_LIMITS: Record = { }; export function getPlanLimits(plan: string): PlanLimits { - // Legacy-Pläne auf neue Namen mappen - if (plan === "premium") return PLAN_LIMITS.legend; - if (plan === "standard") return PLAN_LIMITS.pro; - return PLAN_LIMITS[(plan as Plan) ?? "free"] ?? PLAN_LIMITS.free; + // Free-Tier ist entfallen — alles außer Legend bekommt Pro-Limits. + // Legacy-Namen: premium → legend, standard → pro. + if (plan === "legend" || plan === "premium") return PLAN_LIMITS.legend; + return PLAN_LIMITS.pro; } /**