diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx index 6500c31..63a6260 100644 --- a/apps/rebreak-native/app/(app)/blocker.tsx +++ b/apps/rebreak-native/app/(app)/blocker.tsx @@ -52,7 +52,7 @@ export default function BlockerScreen() { removeDomain, refresh: refreshDomains, } = useCustomDomains(plan); - const { sync: syncBlocklist } = useBlocklistSync(); + const { sync: syncBlocklist, syncWebContent } = useBlocklistSync(); // Realtime: Domain-Submission-Status (approved/rejected/in_review) live patchen. const onDomainChange = useCallback(async () => { @@ -102,13 +102,25 @@ export default function BlockerScreen() { }); }, [urlFilterActive, syncBlocklist, refresh]); + // Layer 2 / VIP: webContent-Domain-Liste IMMER beim Mount syncen — ungated, + // da Layer 2 an Family Controls hängt, nicht am URL-Filter. + const webContentSyncedRef = useRef(false); + useEffect(() => { + if (webContentSyncedRef.current) return; + webContentSyncedRef.current = true; + syncWebContent(); + }, [syncWebContent]); + // Wenn User aus System-Settings zurückkommt (z.B. nach a11y-Aktivierung) → State neu laden. useEffect(() => { const sub = AppState.addEventListener('change', (next) => { - if (next === 'active') refresh(); + if (next === 'active') { + refresh(); + syncWebContent(); + } }); return () => sub.remove(); - }, [refresh]); + }, [refresh, syncWebContent]); // ─── Activate-Handler pro Layer ────────────────────────────────────── @@ -224,6 +236,7 @@ export default function BlockerScreen() { async function handleRemoveWebDomain(id: string) { const result = await removeDomain(id); if (result.ok) { + syncWebContent(); const sync = await syncBlocklist(); if (sync.ok) refresh(); } @@ -388,6 +401,7 @@ export default function BlockerScreen() { onAdd={async (pattern, kind) => { const result = await addDomain(pattern, kind); if (result.ok) { + syncWebContent(); const sync = await syncBlocklist(); if (sync.ok) refresh(); } diff --git a/apps/rebreak-native/hooks/useBlocklistSync.ts b/apps/rebreak-native/hooks/useBlocklistSync.ts index 9643b22..8cb7dd7 100644 --- a/apps/rebreak-native/hooks/useBlocklistSync.ts +++ b/apps/rebreak-native/hooks/useBlocklistSync.ts @@ -18,17 +18,39 @@ type SyncResult = { ok: boolean; count?: number; plan?: string; error?: string } * * Backend respondet 304 wenn ETag matched → kein Re-Download. * - * iOS-Layer-2: am selben Trigger wird auch die kuratierte webContent-Gambling- - * Domain-Liste vom Backend gesynct (`syncWebContentDomains` → - * `webcontent-domains.json` im App-Group-Cache). Best-effort und entkoppelt: - * solange der Layer-2-Endpoint nicht deployed ist, schlägt dieser Sync fehl — - * das beeinflusst das Blocklist-Sync-Ergebnis NICHT (der native - * loadWebContentDomains fällt sauber auf die gebündelte JSON zurück). + * iOS-Layer-2 / VIP: die kuratierte webContent-Domain-Liste wird über die + * SEPARATE `syncWebContent`-Funktion gesynct — bewusst NICHT an `sync()` + * gekoppelt. Layer 2 hängt an Family Controls, nicht am URL-Filter; der Sync + * muss daher ungated laufen (auch wenn der URL-Filter aus ist). */ export function useBlocklistSync() { const [syncing, setSyncing] = useState(false); const [lastResult, setLastResult] = useState(null); + // iOS-Layer-2 / VIP-Liste: kuratierte webContent-Domain-Liste vom Backend + // syncen. ENTKOPPELT vom Blocklist-Sync — Layer 2 hängt an Family Controls, + // NICHT am URL-Filter, also ungated aufrufbar. Best-effort: schlägt es fehl, + // greift nativ der gebündelte Fallback (loadWebContentDomains). + const syncWebContent = useCallback(async (): Promise => { + if (Platform.OS !== 'ios') return; + const baseURL = Constants.expoConfig?.extra?.apiUrl as string; + const session = (await supabase.auth.getSession()).data.session; + const authToken = session?.access_token; + if (!baseURL || !authToken) { + console.warn('[webcontent-sync] skipped — missing baseURL/token'); + return; + } + try { + const res = await protection.syncWebContentDomains({ baseURL, authToken }); + console.log('[webcontent-sync] ok:', JSON.stringify(res)); + } catch (e: any) { + console.warn( + '[webcontent-sync] failed (bundled fallback active):', + e?.message ?? e, + ); + } + }, []); + const sync = useCallback(async (): Promise => { if (syncing) return { ok: false, error: 'already_syncing' }; setSyncing(true); @@ -43,24 +65,6 @@ export function useBlocklistSync() { return result; } - // iOS-Layer-2: webContent-Domain-Liste am selben Trigger mitsyncen. - // Bewusst NICHT awaited mit dem Blocklist-Sync gekoppelt — ein - // Fehlschlag (z.B. Endpoint noch nicht deployed) darf das Blocklist- - // Ergebnis nicht kippen. Fallback auf die gebündelte JSON greift nativ. - if (Platform.OS === 'ios') { - protection - .syncWebContentDomains({ baseURL, authToken }) - .then((res) => - console.log('[webcontent-sync] ok:', JSON.stringify(res)), - ) - .catch((e: any) => - console.warn( - '[webcontent-sync] failed (bundled fallback active):', - e?.message ?? e, - ), - ); - } - const res = await protection.syncBlocklist({ baseURL, authToken }); const result = { ok: true, count: res.count, plan: res.plan }; setLastResult(result); @@ -76,5 +80,5 @@ export function useBlocklistSync() { } }, [syncing]); - return { sync, syncing, lastResult }; + return { sync, syncWebContent, syncing, lastResult }; }