From 33f411ab550063b66551c32de62e901f8f2f5258 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 11 May 2026 17:12:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(blocker):=20post-cooldown=20disable=20show?= =?UTF-8?q?s=20a11y-settings=20notice=20(DiGA=20=E2=80=94=20user=20must=20?= =?UTF-8?q?be=20able=20to=20fully=20exit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the cooldown elapses and forceDisable() runs (VPN off + tamper-lock disarmed), Android's a11y service can't deactivate itself — surface a friendly Alert routing the user to Settings → Accessibility so they can finish removing protection. Wired into both the fetchState cooldown active→inactive transition and the AppState 'active' check; idempotent via ref. (Native side — disable() also disarms the tamper-lock, RebreakAccessibilityService goes fully passive when neither tamper-locked nor enabled, syncBlocklist no longer re-starts the VpnService when disabled — lives in the gitignored module/android dir, not committed here.) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../hooks/useProtectionState.ts | 36 ++++++++++++++++--- apps/rebreak-native/locales/de.json | 5 ++- apps/rebreak-native/locales/en.json | 5 ++- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/rebreak-native/hooks/useProtectionState.ts b/apps/rebreak-native/hooks/useProtectionState.ts index 305310e..f6186cd 100644 --- a/apps/rebreak-native/hooks/useProtectionState.ts +++ b/apps/rebreak-native/hooks/useProtectionState.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { AppState, type AppStateStatus } from 'react-native'; +import { Alert, AppState, type AppStateStatus } from 'react-native'; +import { useTranslation } from 'react-i18next'; import { protection, type ProtectionState, @@ -39,6 +40,7 @@ type UseProtectionStateReturn = { * - Layer-Change-Listener vom Native-Modul (Bypass-Detection) */ export function useProtectionState(): UseProtectionStateReturn { + const { t } = useTranslation(); const [state, setState] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -47,6 +49,30 @@ export function useProtectionState(): UseProtectionStateReturn { const pollTimer = useRef | null>(null); const tickTimer = useRef | null>(null); const prevCooldownActiveRef = useRef(null); + // Verhindert Mehrfach-Alert wenn fetchState + AppState-Listener beide kurz + // hintereinander applyCooldownDisableIfElapsed → true sehen. + const cooldownDisabledNoticeShownRef = useRef(false); + + // Freundlicher Hinweis nachdem der Cooldown abgelaufen ist und der Schutz + // (inkl. Tamper-Lock) abgeschaltet wurde. Android: a11y-Service kann sich + // nicht selbst deaktivieren → User zu den Einstellungen leiten. + const showCooldownElapsedNotice = useCallback(() => { + if (cooldownDisabledNoticeShownRef.current) return; + cooldownDisabledNoticeShownRef.current = true; + Alert.alert( + t('blocker.cooldown_elapsed_title'), + t('blocker.cooldown_elapsed_message'), + [ + { text: t('common.ok'), style: 'cancel' }, + { + text: t('blocker.cooldown_elapsed_open_settings'), + onPress: () => { + protection.openSystemSettings('accessibility').catch(() => {}); + }, + }, + ], + ); + }, [t]); const fetchState = useCallback(async (showLoading = false) => { if (showLoading) setLoading(true); @@ -59,6 +85,7 @@ export function useProtectionState(): UseProtectionStateReturn { if (prevActive === true && !next.cooldown.active) { const didDisable = await protection.applyCooldownDisableIfElapsed(); if (didDisable) { + showCooldownElapsedNotice(); // Nativer State hat sich geändert → ein weiterer Fetch für konsistenten State. const afterDisable = await protection.getCombinedState(); setState(afterDisable); @@ -76,7 +103,7 @@ export function useProtectionState(): UseProtectionStateReturn { } finally { if (showLoading) setLoading(false); } - }, []); + }, [showCooldownElapsedNotice]); // Initial fetch useEffect(() => { @@ -117,11 +144,12 @@ export function useProtectionState(): UseProtectionStateReturn { useEffect(() => { const sub = AppState.addEventListener('change', async (status: AppStateStatus) => { if (status !== 'active') return; - await protection.applyCooldownDisableIfElapsed(); + const didDisable = await protection.applyCooldownDisableIfElapsed(); + if (didDisable) showCooldownElapsedNotice(); await fetchState(false); }); return () => sub.remove(); - }, [fetchState]); + }, [fetchState, showCooldownElapsedNotice]); // Native Layer-Change-Listener (User schaltet VPN extern aus etc.) useEffect(() => { diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index d347aa9..8ebfadf 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -295,7 +295,10 @@ "faq3_a": "Ja. Über die Domain-Liste auf der Blocker-Seite kannst du eigene Domains hinzufügen, die zusätzlich zur globalen Liste blockiert werden.", "faq4_q": "Warum kann ich den Schutz nicht sofort abschalten?", "faq4_a": "Wenn du im Drang bist, willst du oft schnell deaktivieren — und es danach bereuen. Der 24-Stunden-Cooldown gibt dir Zeit, den Drang abklingen zu lassen. Du kannst den Cooldown jederzeit abbrechen — der Schutz bleibt dann einfach an.", - "more_info_title": "Schutz deaktivieren" + "more_info_title": "Schutz deaktivieren", + "cooldown_elapsed_title": "Schutz ist aus", + "cooldown_elapsed_message": "Der Cooldown ist abgelaufen — der Schutz wurde deaktiviert. Du kannst den ReBreak-Bedienungshilfe-Dienst jetzt in den Einstellungen ausschalten.", + "cooldown_elapsed_open_settings": "Einstellungen öffnen" }, "mail": { "title": "Mail-Schutz", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index b915674..2c0816b 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -295,7 +295,10 @@ "faq3_a": "Yes. From the domain list on the blocker page you can add custom domains that get blocked in addition to the global list.", "faq4_q": "Why can't I turn protection off immediately?", "faq4_a": "In the moment of urge, you often want to disable fast — and regret it after. The 24-hour cooldown gives you time for the urge to pass. You can cancel the cooldown anytime — protection then simply stays on.", - "more_info_title": "Disable protection" + "more_info_title": "Disable protection", + "cooldown_elapsed_title": "Protection is off", + "cooldown_elapsed_message": "The cooldown has elapsed — protection was disabled. You can now turn off the ReBreak accessibility service in Settings.", + "cooldown_elapsed_open_settings": "Open Settings" }, "mail": { "title": "Mail Shield",