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:
parent
bc65c7172c
commit
c3390a0fed
@ -52,7 +52,7 @@ export default function BlockerScreen() {
|
|||||||
removeDomain,
|
removeDomain,
|
||||||
refresh: refreshDomains,
|
refresh: refreshDomains,
|
||||||
} = useCustomDomains(plan);
|
} = useCustomDomains(plan);
|
||||||
const { sync: syncBlocklist } = useBlocklistSync();
|
const { sync: syncBlocklist, syncWebContent } = useBlocklistSync();
|
||||||
|
|
||||||
// Realtime: Domain-Submission-Status (approved/rejected/in_review) live patchen.
|
// Realtime: Domain-Submission-Status (approved/rejected/in_review) live patchen.
|
||||||
const onDomainChange = useCallback(async () => {
|
const onDomainChange = useCallback(async () => {
|
||||||
@ -102,13 +102,25 @@ export default function BlockerScreen() {
|
|||||||
});
|
});
|
||||||
}, [urlFilterActive, syncBlocklist, refresh]);
|
}, [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.
|
// Wenn User aus System-Settings zurückkommt (z.B. nach a11y-Aktivierung) → State neu laden.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = AppState.addEventListener('change', (next) => {
|
const sub = AppState.addEventListener('change', (next) => {
|
||||||
if (next === 'active') refresh();
|
if (next === 'active') {
|
||||||
|
refresh();
|
||||||
|
syncWebContent();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return () => sub.remove();
|
return () => sub.remove();
|
||||||
}, [refresh]);
|
}, [refresh, syncWebContent]);
|
||||||
|
|
||||||
// ─── Activate-Handler pro Layer ──────────────────────────────────────
|
// ─── Activate-Handler pro Layer ──────────────────────────────────────
|
||||||
|
|
||||||
@ -224,6 +236,7 @@ export default function BlockerScreen() {
|
|||||||
async function handleRemoveWebDomain(id: string) {
|
async function handleRemoveWebDomain(id: string) {
|
||||||
const result = await removeDomain(id);
|
const result = await removeDomain(id);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
syncWebContent();
|
||||||
const sync = await syncBlocklist();
|
const sync = await syncBlocklist();
|
||||||
if (sync.ok) refresh();
|
if (sync.ok) refresh();
|
||||||
}
|
}
|
||||||
@ -388,6 +401,7 @@ export default function BlockerScreen() {
|
|||||||
onAdd={async (pattern, kind) => {
|
onAdd={async (pattern, kind) => {
|
||||||
const result = await addDomain(pattern, kind);
|
const result = await addDomain(pattern, kind);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
syncWebContent();
|
||||||
const sync = await syncBlocklist();
|
const sync = await syncBlocklist();
|
||||||
if (sync.ok) refresh();
|
if (sync.ok) refresh();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,17 +18,39 @@ type SyncResult = { ok: boolean; count?: number; plan?: string; error?: string }
|
|||||||
*
|
*
|
||||||
* Backend respondet 304 wenn ETag matched → kein Re-Download.
|
* Backend respondet 304 wenn ETag matched → kein Re-Download.
|
||||||
*
|
*
|
||||||
* iOS-Layer-2: am selben Trigger wird auch die kuratierte webContent-Gambling-
|
* iOS-Layer-2 / VIP: die kuratierte webContent-Domain-Liste wird über die
|
||||||
* Domain-Liste vom Backend gesynct (`syncWebContentDomains` →
|
* SEPARATE `syncWebContent`-Funktion gesynct — bewusst NICHT an `sync()`
|
||||||
* `webcontent-domains.json` im App-Group-Cache). Best-effort und entkoppelt:
|
* gekoppelt. Layer 2 hängt an Family Controls, nicht am URL-Filter; der Sync
|
||||||
* solange der Layer-2-Endpoint nicht deployed ist, schlägt dieser Sync fehl —
|
* muss daher ungated laufen (auch wenn der URL-Filter aus ist).
|
||||||
* das beeinflusst das Blocklist-Sync-Ergebnis NICHT (der native
|
|
||||||
* loadWebContentDomains fällt sauber auf die gebündelte JSON zurück).
|
|
||||||
*/
|
*/
|
||||||
export function useBlocklistSync() {
|
export function useBlocklistSync() {
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
const [lastResult, setLastResult] = useState<SyncResult | null>(null);
|
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> => {
|
const sync = useCallback(async (): Promise<SyncResult> => {
|
||||||
if (syncing) return { ok: false, error: 'already_syncing' };
|
if (syncing) return { ok: false, error: 'already_syncing' };
|
||||||
setSyncing(true);
|
setSyncing(true);
|
||||||
@ -43,24 +65,6 @@ export function useBlocklistSync() {
|
|||||||
return result;
|
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 res = await protection.syncBlocklist({ baseURL, authToken });
|
||||||
const result = { ok: true, count: res.count, plan: res.plan };
|
const result = { ok: true, count: res.count, plan: res.plan };
|
||||||
setLastResult(result);
|
setLastResult(result);
|
||||||
@ -76,5 +80,5 @@ export function useBlocklistSync() {
|
|||||||
}
|
}
|
||||||
}, [syncing]);
|
}, [syncing]);
|
||||||
|
|
||||||
return { sync, syncing, lastResult };
|
return { sync, syncWebContent, syncing, lastResult };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user