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",