diff --git a/apps/rebreak-native/app/debug.tsx b/apps/rebreak-native/app/debug.tsx
index ff25b2d..f4bd264 100644
--- a/apps/rebreak-native/app/debug.tsx
+++ b/apps/rebreak-native/app/debug.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
-import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
+import { View, Text, ScrollView, Switch, TouchableOpacity, Alert } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
@@ -7,6 +7,7 @@ import { useColors } from '../lib/theme';
import { useMe, invalidateMe, type Plan } from '../hooks/useMe';
import { apiFetch } from '../lib/api';
import { PlanChangeSheet } from '../components/plan/PlanChangeSheet';
+import { getCooldownTestMode, setCooldownTestMode } from '../lib/protection';
export default function DebugScreen() {
const router = useRouter();
@@ -95,6 +96,8 @@ export default function DebugScreen() {
/>
) : null}
+
+
{
+ getCooldownTestMode().then(setEnabled);
+ }, []);
+
+ async function toggle(value: boolean) {
+ setEnabled(value);
+ await setCooldownTestMode(value);
+ }
+
+ return (
+
+
+
+
+
+
+
+ Test-Cooldown (40 Sek)
+
+
+
+
+ Nur auf staging — der nächste "Cooldown starten" nutzt dann 40 Sekunden statt 24h.
+
+
+
+ );
+}
+
function DebugStub({
title,
subtitle,
diff --git a/apps/rebreak-native/components/blocker/DeactivationExplainerSheet.tsx b/apps/rebreak-native/components/blocker/DeactivationExplainerSheet.tsx
index 8b462c2..371a5c6 100644
--- a/apps/rebreak-native/components/blocker/DeactivationExplainerSheet.tsx
+++ b/apps/rebreak-native/components/blocker/DeactivationExplainerSheet.tsx
@@ -1,4 +1,5 @@
-import { Modal, View, Text, Pressable, TouchableOpacity, ScrollView, ActionSheetIOS, Platform, Alert } from 'react-native';
+import { Modal, View, Text, TouchableOpacity, ScrollView, ActionSheetIOS, Platform, Alert } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -28,6 +29,7 @@ export function DeactivationExplainerSheet({
}: Props) {
const { t } = useTranslation();
const colors = useColors();
+ const insets = useSafeAreaInsets();
const [submitting, setSubmitting] = useState(false);
function showFinalConfirm() {
@@ -77,31 +79,37 @@ export function DeactivationExplainerSheet({
onRequestClose={onClose}
>
- {/* Header */}
+ {/* Header — paddingTop berücksichtigt Notch/Statusbar (pageSheet auf iOS gibt
+ insets korrekt weiter; auf Android sichert es den Statusbar-Bereich ab). */}
-
+
{t('common.back')}
-
+
{t('blocker.deactivation_heading')}
-
+
{t('blocker.deactivation_title')}
diff --git a/apps/rebreak-native/components/blocker/ProtectionDetailsSheet.tsx b/apps/rebreak-native/components/blocker/ProtectionDetailsSheet.tsx
index 3965a28..7a10ae0 100644
--- a/apps/rebreak-native/components/blocker/ProtectionDetailsSheet.tsx
+++ b/apps/rebreak-native/components/blocker/ProtectionDetailsSheet.tsx
@@ -3,7 +3,6 @@ import {
Modal,
View,
Text,
- Pressable,
TouchableOpacity,
ScrollView,
Dimensions,
@@ -12,6 +11,7 @@ import {
ActivityIndicator,
Easing,
} from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import Svg, { Path, Circle } from 'react-native-svg';
@@ -58,6 +58,7 @@ export function ProtectionDetailsSheet({
}: Props) {
const { t, i18n } = useTranslation();
const colors = useColors();
+ const insets = useSafeAreaInsets();
const localeTag = i18n.language === 'de' ? 'de-DE' : 'en-US';
const sheetHeight = useRef(new Animated.Value(DEFAULT_HEIGHT)).current;
@@ -148,8 +149,9 @@ export function ProtectionDetailsSheet({
onRequestClose={handleClose}
statusBarTranslucent
>
-
@@ -202,15 +204,15 @@ export function ProtectionDetailsSheet({
{t('blocker.details_title')}
-
+
{t('blocker.details_done')}
-
+
{loadingStats && !stats ? (
diff --git a/apps/rebreak-native/hooks/useProtectionState.ts b/apps/rebreak-native/hooks/useProtectionState.ts
index 23be816..305310e 100644
--- a/apps/rebreak-native/hooks/useProtectionState.ts
+++ b/apps/rebreak-native/hooks/useProtectionState.ts
@@ -46,11 +46,28 @@ export function useProtectionState(): UseProtectionStateReturn {
const pollTimer = useRef | null>(null);
const tickTimer = useRef | null>(null);
+ const prevCooldownActiveRef = useRef(null);
const fetchState = useCallback(async (showLoading = false) => {
if (showLoading) setLoading(true);
try {
const next = await protection.getCombinedState();
+ const prevActive = prevCooldownActiveRef.current;
+ prevCooldownActiveRef.current = next.cooldown.active;
+
+ // Cooldown ist gerade von active → inactive gekippt: Auto-Disable prüfen.
+ if (prevActive === true && !next.cooldown.active) {
+ const didDisable = await protection.applyCooldownDisableIfElapsed();
+ if (didDisable) {
+ // Nativer State hat sich geändert → ein weiterer Fetch für konsistenten State.
+ const afterDisable = await protection.getCombinedState();
+ setState(afterDisable);
+ setTickSeconds(afterDisable.cooldown.remainingSeconds);
+ setError(null);
+ return;
+ }
+ }
+
setState(next);
setTickSeconds(next.cooldown.remainingSeconds);
setError(null);
@@ -93,16 +110,15 @@ export function useProtectionState(): UseProtectionStateReturn {
};
}, [state?.cooldown.active]);
- // AppState-Listener: Refresh wenn App aus Background zurückkommt.
- // KEIN auto-disable hier — Backend's canDisableProtection-Flag ist auch
- // in initial-state true, würde sonst den Filter killen ohne dass User
- // jemals einen Cooldown gestartet hat. Auto-Disable nur über expliziten
- // UI-Pfad nach Cooldown-Ablauf (kommt in Step 5b).
+ // AppState-Listener: Refresh + Auto-Disable wenn Cooldown elapsed ist.
+ // Guard in applyCooldownDisableIfElapsed: cooldownEndsAt muss gesetzt sein
+ // (= es lief je ein Cooldown) und remainingSeconds <= 0. Verhindert
+ // False-Positives wenn canDisableProtection im Initial-State true ist.
useEffect(() => {
- const sub = AppState.addEventListener('change', (status: AppStateStatus) => {
- if (status === 'active') {
- fetchState(false);
- }
+ const sub = AppState.addEventListener('change', async (status: AppStateStatus) => {
+ if (status !== 'active') return;
+ await protection.applyCooldownDisableIfElapsed();
+ await fetchState(false);
});
return () => sub.remove();
}, [fetchState]);
diff --git a/apps/rebreak-native/lib/protection.ts b/apps/rebreak-native/lib/protection.ts
index 8c0284f..9895b82 100644
--- a/apps/rebreak-native/lib/protection.ts
+++ b/apps/rebreak-native/lib/protection.ts
@@ -9,6 +9,7 @@
* kümmert sich nur um echten Device-State (NEFilter, Family Controls etc.).
*/
import { Platform } from "react-native";
+import AsyncStorage from "@react-native-async-storage/async-storage";
import RebreakProtection from "../modules/rebreak-protection";
import type {
ActivateResult,
@@ -69,6 +70,19 @@ type BackendProtectionState = {
plan: "free" | "pro" | "legend";
};
+// ─── Dev Helpers ───────────────────────────────────────────────────────────
+
+const DEV_COOLDOWN_TESTMODE_KEY = "dev:cooldown-testmode";
+
+export async function setCooldownTestMode(on: boolean): Promise {
+ await AsyncStorage.setItem(DEV_COOLDOWN_TESTMODE_KEY, on ? "1" : "0");
+}
+
+export async function getCooldownTestMode(): Promise {
+ const val = await AsyncStorage.getItem(DEV_COOLDOWN_TESTMODE_KEY);
+ return val === "1";
+}
+
// ─── Public API ────────────────────────────────────────────────────────────
export const protection = {
@@ -142,15 +156,18 @@ export const protection = {
// ─── Backend-Cooldown ────────────────────────────────────────────────────
- /** Startet 24h Cooldown. Schutz BLEIBT aktiv, kann erst nach Ablauf disabled werden. */
+ /** Startet 24h Cooldown (oder 40s bei aktivem __DEV__-testMode). Schutz BLEIBT aktiv. */
async requestDeactivation(
reason?: string,
): Promise<{ cooldownEndsAt: string }> {
+ const testMode = __DEV__ ? await getCooldownTestMode() : false;
+ const body: Record = { reason };
+ if (testMode) body.testMode = true;
const res = await apiFetch<{
cooldownEndsAt: string;
token: string;
remainingSeconds: number;
- }>("/api/cooldown/request", { method: "POST", body: { reason } });
+ }>("/api/cooldown/request", { method: "POST", body });
return { cooldownEndsAt: res.cooldownEndsAt };
},