feat(blocker): post-cooldown disable shows a11y-settings notice (DiGA — user must be able to fully exit)
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) <noreply@anthropic.com>
This commit is contained in:
parent
aac709ec41
commit
33f411ab55
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
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 {
|
import {
|
||||||
protection,
|
protection,
|
||||||
type ProtectionState,
|
type ProtectionState,
|
||||||
@ -39,6 +40,7 @@ type UseProtectionStateReturn = {
|
|||||||
* - Layer-Change-Listener vom Native-Modul (Bypass-Detection)
|
* - Layer-Change-Listener vom Native-Modul (Bypass-Detection)
|
||||||
*/
|
*/
|
||||||
export function useProtectionState(): UseProtectionStateReturn {
|
export function useProtectionState(): UseProtectionStateReturn {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [state, setState] = useState<ProtectionState | null>(null);
|
const [state, setState] = useState<ProtectionState | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -47,6 +49,30 @@ export function useProtectionState(): UseProtectionStateReturn {
|
|||||||
const pollTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const tickTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
const tickTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const prevCooldownActiveRef = useRef<boolean | null>(null);
|
const prevCooldownActiveRef = useRef<boolean | null>(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) => {
|
const fetchState = useCallback(async (showLoading = false) => {
|
||||||
if (showLoading) setLoading(true);
|
if (showLoading) setLoading(true);
|
||||||
@ -59,6 +85,7 @@ export function useProtectionState(): UseProtectionStateReturn {
|
|||||||
if (prevActive === true && !next.cooldown.active) {
|
if (prevActive === true && !next.cooldown.active) {
|
||||||
const didDisable = await protection.applyCooldownDisableIfElapsed();
|
const didDisable = await protection.applyCooldownDisableIfElapsed();
|
||||||
if (didDisable) {
|
if (didDisable) {
|
||||||
|
showCooldownElapsedNotice();
|
||||||
// Nativer State hat sich geändert → ein weiterer Fetch für konsistenten State.
|
// Nativer State hat sich geändert → ein weiterer Fetch für konsistenten State.
|
||||||
const afterDisable = await protection.getCombinedState();
|
const afterDisable = await protection.getCombinedState();
|
||||||
setState(afterDisable);
|
setState(afterDisable);
|
||||||
@ -76,7 +103,7 @@ export function useProtectionState(): UseProtectionStateReturn {
|
|||||||
} finally {
|
} finally {
|
||||||
if (showLoading) setLoading(false);
|
if (showLoading) setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [showCooldownElapsedNotice]);
|
||||||
|
|
||||||
// Initial fetch
|
// Initial fetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -117,11 +144,12 @@ export function useProtectionState(): UseProtectionStateReturn {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = AppState.addEventListener('change', async (status: AppStateStatus) => {
|
const sub = AppState.addEventListener('change', async (status: AppStateStatus) => {
|
||||||
if (status !== 'active') return;
|
if (status !== 'active') return;
|
||||||
await protection.applyCooldownDisableIfElapsed();
|
const didDisable = await protection.applyCooldownDisableIfElapsed();
|
||||||
|
if (didDisable) showCooldownElapsedNotice();
|
||||||
await fetchState(false);
|
await fetchState(false);
|
||||||
});
|
});
|
||||||
return () => sub.remove();
|
return () => sub.remove();
|
||||||
}, [fetchState]);
|
}, [fetchState, showCooldownElapsedNotice]);
|
||||||
|
|
||||||
// Native Layer-Change-Listener (User schaltet VPN extern aus etc.)
|
// Native Layer-Change-Listener (User schaltet VPN extern aus etc.)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -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.",
|
"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_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.",
|
"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": {
|
"mail": {
|
||||||
"title": "Mail-Schutz",
|
"title": "Mail-Schutz",
|
||||||
|
|||||||
@ -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.",
|
"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_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.",
|
"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": {
|
"mail": {
|
||||||
"title": "Mail Shield",
|
"title": "Mail Shield",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user