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>
67 lines
2.1 KiB
TypeScript
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 };
|
|
}
|