rebreak-monorepo/apps/rebreak-native/hooks/useWebContentDomains.ts
chahinebrini fe156a5f58 feat(blocker/vip): Freigabe-Button, landabhängige VIP-Liste, Hybrid-Komposition + Add-Check
Blocker-UI:
- FilterTile: Trash-Button → status-aware Freigabe-Button (Freigeben/Erneut/
  in-Prüfung); RemoveDomainSheet entfernt — kein Domain-Entfernen mehr in der UI
- VIP-Liste landabhängig: zeigt die komponierte Endpoint-Liste statt nur
  eigener Customs; Land über Geräte-Region (expo-localization)
- VIP-Realtime: refetch bei Domain-Add/Approve/Reject, pulsierender Ring
  für neue/active/submitted Chips

VIP-Komposition (webcontent-domains):
- Hybrid: Customs auf 30 gekappt, 20 Slots fest für die kuratierte Top-Liste
  reserviert — Customs können die Top-Gambling-Domains nicht verdrängen

Add-Check (custom-domains POST), für web reaktiviert — 3 Fälle gegen
Layer 1 (global) + Layer 2 (kuratierte VIP):
- weder global noch kuratiert → normaler active-Eintrag
- global + kuratiert → alreadyProtected, kein Slot
- global, nicht kuratiert → inGlobalNotVip; per addToVip als status=approved
  speicherbar (kein Slot, nur VIP-Liste)

DE-Gambling-Liste 30→36, nach Relevanz sortiert (erste 20 = reservierte Plätze)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:27:10 +02:00

67 lines
2.1 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import * as Localization from 'expo-localization';
import { apiFetch } from '../lib/api';
/**
* Landabhängige VIP-Layer-2-Liste.
*
* Das Backend (`GET /api/protection/webcontent-domains`) liefert die
* komponierte Liste pro Land (Custom-Domains gekappt auf 30 + kuratierte
* Auffüllung, dedup, Cap 50). Hier wählen wir die Country-Slice nach der
* GERÄTE-Region aus — dasselbe Signal, das auch der native iOS-webContent-
* Filter nutzt (`Locale.current.region`).
*/
const VIP_COUNTRIES = ['DE', 'GB', 'FR'] as const;
export type VipCountry = (typeof VIP_COUNTRIES)[number];
/** Geräte-Region → unterstützter VIP-Ländercode. Fallback DE. */
export function resolveVipCountry(): VipCountry {
const region = Localization.getLocales()[0]?.regionCode?.toUpperCase();
if (region && (VIP_COUNTRIES as readonly string[]).includes(region)) {
return region as VipCountry;
}
return 'DE';
}
type WebContentResponse = { _meta?: unknown } & Record<string, string[]>;
export function useWebContentDomains() {
const [country] = useState<VipCountry>(resolveVipCountry);
const [domains, setDomains] = useState<string[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
const refetch = useCallback(async () => {
try {
const res = await apiFetch<WebContentResponse>(
'/api/protection/webcontent-domains',
);
if (!mountedRef.current) return;
const list = Array.isArray(res?.[country]) ? res[country] : [];
setDomains(list);
setError(null);
} catch (e: any) {
if (!mountedRef.current) return;
console.warn('[useWebContentDomains] fetch failed:', e?.message ?? e);
setError(e?.message ?? 'fetch_failed');
} finally {
if (mountedRef.current) setLoading(false);
}
}, [country]);
useEffect(() => {
refetch();
}, [refetch]);
return { country, domains, loading, error, refetch };
}