chahinebrini c3390a0fed fix(blocker): webContent-Sync von URL-Filter entkoppeln
syncWebContentDomains war als Side-Effect an syncBlocklist gehaengt, das nur
bei aktivem URL-Filter laeuft. Layer 2 haengt aber an Family Controls — der
Sync lief nie wenn nur App-Lock/FC aktiv war. Jetzt eigene syncWebContent-
Funktion, ungated: Mount + App-Foreground + nach Domain-Add/-Remove.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:54:11 +02:00

85 lines
3.2 KiB
TypeScript

import { useCallback, useState } from 'react';
import { Platform } from 'react-native';
import Constants from 'expo-constants';
import { supabase } from '../lib/supabase';
import { protection } from '../lib/protection';
type SyncResult = { ok: boolean; count?: number; plan?: string; error?: string };
/**
* Synct die binary Blocklist (`blocklist.bin`) vom Server in die App-Group.
* Die NEFilter-Extension memory-mapped diese Datei — ohne Sync = leere
* Blocklist = nichts wird geblockt.
*
* Triggers:
* - direkt nach activateUrlFilter() success
* - nach Domain-Add/-Submit/-Delete
* - bei App-Resume (in case Server-Updates kamen)
*
* Backend respondet 304 wenn ETag matched → kein Re-Download.
*
* 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<SyncResult | null>(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<void> => {
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<SyncResult> => {
if (syncing) return { ok: false, error: 'already_syncing' };
setSyncing(true);
try {
const baseURL = Constants.expoConfig?.extra?.apiUrl as string;
const session = (await supabase.auth.getSession()).data.session;
const authToken = session?.access_token;
if (!baseURL || !authToken) {
const result = { ok: false, error: 'missing_baseURL_or_token' };
setLastResult(result);
return result;
}
const res = await protection.syncBlocklist({ baseURL, authToken });
const result = { ok: true, count: res.count, plan: res.plan };
setLastResult(result);
console.log('[blocklist-sync] ok:', res);
return result;
} catch (e: any) {
const result = { ok: false, error: e?.message ?? 'sync_failed' };
setLastResult(result);
console.error('[blocklist-sync] failed:', e);
return result;
} finally {
setSyncing(false);
}
}, [syncing]);
return { sync, syncWebContent, syncing, lastResult };
}