feat(blocker): reactivation only re-arms the VPN/filter; a11y setup is first-time-only

The a11y (App-Lock) permission flow now runs only the first time the user turns
protection on. Reactivating after a cooldown / external disable just re-starts the
VPN/DNS filter — no a11y system prompt, no modal loop ("a11y can't be activated…").

- blocker.tsx handleActivateFamilyControls: no error modal when error === 'accessibility_pending'
  (we just opened the a11y settings — that's the feedback; tapping again re-opens, no loop).
- lib/protection.ts getCombinedState: "active" = urlFilter on (App-Lock is optional hardening,
  not a precondition); "recoveringFromBypass" now means urlFilter is OFF while the backend
  says it should be on (a real bypass), instead of "lock is off".
- blocker.tsx recoveringFromBypass alert: offers "turn back on" → activateUrlFilter (VPN),
  not activateFamilyControls.
- _layout.tsx bypass re-arm (enforceProtection fallback + onBypassNotificationTap):
  protection.activate() instead of activateFamilyControls().
- new i18n keys: blocker.protection_off_title / protection_off_message / reactivate_btn.

JS-only (hot-reloadable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-11 18:46:21 +02:00
parent 3c2aee7bda
commit 4492c7b265
5 changed files with 34 additions and 20 deletions

View File

@ -92,10 +92,12 @@ export default function AppLayout() {
bypassNotifiedRef.current = true;
const notified = await notifyBypassDetected();
if (!notified) {
// Fallback wenn Notifications nicht erlaubt sind.
// Fallback wenn Notifications nicht erlaubt sind. Reaktivierung setzt
// NUR den Filter/VPN wieder — kein a11y-Prompt (das passiert nur beim
// ersten Einrichten).
rearmInFlightRef.current = true;
router.replace('/blocker');
await protection.activateFamilyControls().catch(() => ({ enabled: false }));
await protection.activate().catch(() => null);
}
} finally {
rearmInFlightRef.current = false;
@ -107,7 +109,8 @@ export default function AppLayout() {
rearmInFlightRef.current = true;
try {
router.replace('/blocker');
await protection.activateFamilyControls().catch(() => ({ enabled: false }));
// Reaktivierung = nur Filter/VPN wieder setzen (a11y nur beim ersten Mal).
await protection.activate().catch(() => null);
} finally {
rearmInFlightRef.current = false;
}

View File

@ -134,12 +134,17 @@ export default function BlockerScreen() {
try {
const result = await activateFamilyControls();
console.log('[blocker] activateFamilyControls:', result);
if (!result.enabled) {
// `accessibility_pending` = die a11y-Berechtigung fehlt noch und wir haben
// grad die System-Settings geöffnet → das IST das Feedback. Kein Fehler-
// Modal (sonst Modal-Loop bei jedem Tap). a11y wird nur beim ersten
// Einrichten geholt; danach ist das hier ein 1-Tap-Arm ohne Dialog.
if (!result.enabled && result.error !== 'accessibility_pending') {
Alert.alert(
t('blocker.activate_app_lock_failed_title'),
result.error ?? t('blocker.activate_app_lock_failed_msg'),
);
}
return result;
} catch (e: any) {
console.error('[blocker] activateFamilyControls threw:', e);
Alert.alert(t('blocker.activation_failed_title'), e?.message ?? t('common.unknown_error'));
@ -188,16 +193,15 @@ export default function BlockerScreen() {
}
if (bypassAlertShownRef.current) return;
bypassAlertShownRef.current = true;
// Schutz-Filter ist aus, sollte aber an sein → Reaktivierung setzt NUR den
// VPN/Filter wieder (kein a11y-Prompt — das passiert nur beim ersten Mal).
Alert.alert(
t('blocker.activate_app_lock_failed_title'),
t('blocker.layers_app_lock_warning'),
[{
text: t('common.ok'),
onPress: () => {
void handleActivateFamilyControls();
},
}],
{ cancelable: false },
t('blocker.protection_off_title'),
t('blocker.protection_off_message'),
[
{ text: t('common.ok'), style: 'cancel' },
{ text: t('blocker.reactivate_btn'), onPress: () => { void handleActivateUrlFilter(); } },
],
);
}, [state?.phase, t]);

View File

@ -243,16 +243,17 @@ export const protection = {
} as DeviceLayers)
: rawLayers;
const allLayersOn = isAllLayersOn(layers);
const iosLockActive =
layers.appDeletionLock ?? layers.familyControls ?? false;
// "Aktiv" = der eigentliche Schutz (URL-/DNS-Filter) läuft. Der App-Lock
// (familyControls/tamperLock) ist optionales Hardening — er macht den Schutz
// schwerer abschaltbar, ist aber keine Voraussetzung für "geschützt". Er wird
// nur beim ersten Aktivieren eingerichtet; eine Reaktivierung setzt nur den
// Filter wieder. → "recoveringFromBypass" heißt deshalb: Filter ist aus,
// obwohl das Backend sagt er sollte an sein (= jemand hat den VPN extern aus).
const phase: ProtectionPhase = cooldown.active
? "cooldownActive"
: backend?.protectionShouldBeActive === true &&
layers.urlFilter === true &&
iosLockActive !== true
: backend?.protectionShouldBeActive === true && layers.urlFilter !== true
? "recoveringFromBypass"
: allLayersOn
: layers.urlFilter === true
? "active"
: "inactive";

View File

@ -241,6 +241,9 @@
"activate_url_failed_title": "URL-Filter konnte nicht aktiviert werden",
"activate_url_failed_msg": "Unbekannter Fehler.\nDu kannst es nochmal versuchen oder System-Einstellungen prüfen.",
"activate_settings_btn": "Einstellungen",
"protection_off_title": "Schutz ist aus",
"protection_off_message": "Der Filter läuft gerade nicht, sollte aber an sein. Willst du ihn wieder einschalten?",
"reactivate_btn": "Wieder einschalten",
"activate_app_lock_failed_title": "App-Lock konnte nicht aktiviert werden",
"activate_app_lock_failed_msg": "Die nötige Berechtigung wurde verweigert. Du kannst es nochmal versuchen.",
"sync_list_failed_title": "Filter-Liste konnte nicht geladen werden",

View File

@ -241,6 +241,9 @@
"activate_url_failed_title": "Could not activate URL filter",
"activate_url_failed_msg": "Unknown error.\nYou can try again or check System Settings.",
"activate_settings_btn": "Settings",
"protection_off_title": "Protection is off",
"protection_off_message": "The filter isn't running but should be. Want to turn it back on?",
"reactivate_btn": "Turn back on",
"activate_app_lock_failed_title": "Could not activate App Lock",
"activate_app_lock_failed_msg": "The required permission was denied. You can try again.",
"sync_list_failed_title": "Filter list could not be loaded",