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>
This commit is contained in:
chahinebrini 2026-05-21 21:54:11 +02:00
parent bc65c7172c
commit c3390a0fed
2 changed files with 46 additions and 28 deletions

View File

@ -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();
}

View File

@ -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<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);
@ -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 };
}