diff --git a/apps/rebreak-native/app.config.ts b/apps/rebreak-native/app.config.ts
index 37a4c18..6cf20cf 100644
--- a/apps/rebreak-native/app.config.ts
+++ b/apps/rebreak-native/app.config.ts
@@ -131,12 +131,19 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
},
},
},
- // Family Controls Entitlement (denyAppRemoval via ManagedSettings) ist seit
- // 2026-05 für ReBreak via Apple-Entitlement-Request approved und in TestFlight-
- // sowie production-Builds aktiv. Daher hart auf `true` — keine Build-Flag-Gating
- // mehr nötig. Legacy `REBREAK_ENABLE_FAMILY_CONTROLS=1` aus dev-builds wird
- // ignoriert (das war Übergangs-Gating vor Apple-Approval).
- familyControlsEnabled: true,
+ // Family Controls (denyAppRemoval via ManagedSettings) braucht ein
+ // DISTRIBUTION-Entitlement das noch bei Apple zur Freigabe liegt. Ohne das
+ // Entitlement blockt iOS den ManagedSettingsAgent-XPC auf Sandbox-Ebene
+ // (NSCocoaErrorDomain:4099 "Sandbox restriction" — verifiziert in Build 11).
+ //
+ // Der Wert MUSS mit dem Entitlement-Gating im Plugin übereinstimmen:
+ // plugins/with-rebreak-protection-ios.js setzt das family-controls-Entitlement
+ // NUR wenn REBREAK_ENABLE_FAMILY_CONTROLS==='1'. Diese Flag ist nur in
+ // development-Builds gesetzt. → familyControlsEnabled IDENTISCH koppeln,
+ // sonst bietet die App ein Feature an das im Build sandbox-blockiert ist.
+ // Sobald Apple das Distribution-Entitlement freigibt: Flag in eas.json
+ // preview/production env setzen — dann zieht beides automatisch.
+ familyControlsEnabled: process.env.REBREAK_ENABLE_FAMILY_CONTROLS === "1",
apiUrl:
process.env.EXPO_PUBLIC_API_URL ||
process.env.API_URL ||
diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx
index 474af18..d9c3594 100644
--- a/apps/rebreak-native/app/(app)/blocker.tsx
+++ b/apps/rebreak-native/app/(app)/blocker.tsx
@@ -18,7 +18,7 @@ import { useProtectionState } from '../../hooks/useProtectionState';
import { useCustomDomains } from '../../hooks/useCustomDomains';
import { useBlocklistSync } from '../../hooks/useBlocklistSync';
import { useDomainSubmissionRealtime } from '../../hooks/useDomainSubmissionRealtime';
-import { protection } from '../../lib/protection';
+import { protection, FAMILY_CONTROLS_AVAILABLE } from '../../lib/protection';
import { useColors, type ColorScheme } from '../../lib/theme';
export default function BlockerScreen() {
@@ -79,7 +79,11 @@ export default function BlockerScreen() {
// (Hardening). Family-Controls ALLEINE = kein Schutz, nur denyAppRemoval —
// ohne URL-Filter sieht der User trotzdem Glücksspielseiten. Daher BEIDE
// müssen an sein damit der "Schutz aktiv"-Banner gezeigt wird.
- const lockedIn = urlFilterActive && appDeletionLockActive;
+ // "lockedIn" normal = URL-Filter UND App-Lock aktiv. Wenn Family Controls
+ // build-seitig nicht verfügbar ist (Distribution-Entitlement pending), kann
+ // es keinen App-Lock geben → dann reicht der URL-Filter allein für "geschützt".
+ const lockedIn =
+ urlFilterActive && (appDeletionLockActive || !FAMILY_CONTROLS_AVAILABLE);
const urlFilterActiveRef = useRef(urlFilterActive);
useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]);
@@ -275,7 +279,11 @@ export default function BlockerScreen() {
onActivate={handleActivateFamilyControls}
warning={t('blocker.layers_app_lock_warning')}
/>
- ) : (
+ ) : FAMILY_CONTROLS_AVAILABLE ? (
+ /* iOS App-Lock nur zeigen wenn das Family-Controls-Entitlement
+ im Build aktiv ist. Distribution-Builds ohne Apple-Approval
+ → Card ausblenden statt ein sandbox-blockiertes Feature
+ anzubieten (NSCocoaErrorDomain:4099). */
- )}
+ ) : null}
)}
diff --git a/apps/rebreak-native/app/debug.tsx b/apps/rebreak-native/app/debug.tsx
index 7e1e930..26cec7f 100644
--- a/apps/rebreak-native/app/debug.tsx
+++ b/apps/rebreak-native/app/debug.tsx
@@ -120,6 +120,7 @@ export default function DebugScreen() {
+ {Platform.OS === 'ios' ? : null}
([]);
+ const [loading, setLoading] = useState(false);
+
+ async function loadLogs() {
+ setLoading(true);
+ try {
+ const next = await protection.getProtectionLogs();
+ // neueste oben
+ setLogs([...next].reverse());
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ loadLogs();
+ }, []);
+
+ function copyLogs() {
+ Clipboard.setString(logs.join('\n'));
+ Alert.alert('Kopiert', `${logs.length} Protection-Log-Zeilen in Zwischenablage.`);
+ }
+
+ async function clearLogs() {
+ await protection.clearProtectionLogs();
+ setLogs([]);
+ }
+
+ return (
+
+
+
+
+
+
+ Protection Log (nativ)
+
+
+
+
+
+
+
+
+
+
+
+
+ {logs.length === 0 ? (
+
+ {loading
+ ? 'Lade...'
+ : 'Keine Logs — aktiviere den URL-Filter, dann auf Refresh tippen.'}
+
+ ) : (
+
+
+ {logs.map((line, i) => (
+
+ {line}
+
+ ))}
+
+
+ )}
+
+
+ SharedLogStore — max 200 Einträge, neueste oben
+
+
+ );
+}
+
function LogLine({
entry,
colors,
diff --git a/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx b/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx
index 4cd3aa9..3fcd3db 100644
--- a/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx
+++ b/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useColors } from '../../../lib/theme';
import { apiFetch } from '../../../lib/api';
import { invalidateMe } from '../../../hooks/useMe';
-import { protection } from '../../../lib/protection';
+import { protection, FAMILY_CONTROLS_AVAILABLE } from '../../../lib/protection';
import RebreakProtection from '../../../modules/rebreak-protection';
import { getPermissionScreenshot } from '../../../lib/onboardingAssets';
import { OnboardingShell } from '../OnboardingShell';
@@ -76,6 +76,14 @@ function IosProtectionSlide({
);
return;
}
+ // Family Controls (App-Lock) braucht ein Distribution-Entitlement das
+ // (noch) nicht freigegeben ist → in TestFlight/production-Builds ist
+ // FAMILY_CONTROLS_AVAILABLE=false. Dann den Lock-Step überspringen:
+ // URL-Filter allein = vollwertiger Schutz, der Lock ist nur Hardening.
+ if (!FAMILY_CONTROLS_AVAILABLE) {
+ finishProtectionStep();
+ return;
+ }
setPhase('preexplain_lock');
} finally {
setActivating(false);
@@ -147,7 +155,13 @@ function IosProtectionSlide({
onClose={() => setPermissionDeniedOpen(false)}
onRetry={async () => {
const res = await protection.resetUrlFilter();
- if (res.enabled) setPhase('preexplain_lock');
+ if (res.enabled) {
+ if (!FAMILY_CONTROLS_AVAILABLE) {
+ finishProtectionStep();
+ } else {
+ setPhase('preexplain_lock');
+ }
+ }
return res;
}}
/>
diff --git a/apps/rebreak-native/lib/protection.ts b/apps/rebreak-native/lib/protection.ts
index d750270..e9ac699 100644
--- a/apps/rebreak-native/lib/protection.ts
+++ b/apps/rebreak-native/lib/protection.ts
@@ -214,6 +214,27 @@ export const protection = {
return RebreakProtection.openSystemSettings(target);
},
+ /** iOS: native Protection-Logs (NEFilter/FamilyControls flow) für Debug-Page.
+ * Auf Android/Web no-op → leeres Array. */
+ async getProtectionLogs(): Promise {
+ if (Platform.OS !== "ios") return [];
+ try {
+ return await RebreakProtection.getProtectionLogs();
+ } catch {
+ return [];
+ }
+ },
+
+ /** iOS: leert die nativen Protection-Logs. No-op auf Android/Web. */
+ async clearProtectionLogs(): Promise {
+ if (Platform.OS !== "ios") return;
+ try {
+ await RebreakProtection.clearProtectionLogs();
+ } catch {
+ // ignore
+ }
+ },
+
addLayerChangeListener(cb: (layers: DeviceLayers) => void) {
return RebreakProtection.addListener("onLayerChange", cb);
},
diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift
index 6b72a24..8909503 100644
--- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift
+++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift
@@ -588,6 +588,23 @@ public class RebreakProtectionModule: Module {
}
}
}
+
+ // ───────── getProtectionLogs / clearProtectionLogs ─────────
+ //
+ // SharedLogStore (NEFilter/FamilyControls native flow) schreibt nach
+ // UserDefaults(APP_GROUP) key "url_filter_logs". Die App hat bisher KEINEN
+ // Weg die auszulesen → TestFlight-Debugging war auf Console.app angewiesen.
+ // Dieser Export macht die nativen Logs in der Debug-Page sichtbar.
+
+ AsyncFunction("getProtectionLogs") { () -> [String] in
+ guard let defaults = UserDefaults(suiteName: APP_GROUP) else { return [] }
+ return defaults.stringArray(forKey: SharedLogStore.logKey) ?? []
+ }
+
+ AsyncFunction("clearProtectionLogs") { () -> Void in
+ guard let defaults = UserDefaults(suiteName: APP_GROUP) else { return }
+ defaults.removeObject(forKey: SharedLogStore.logKey)
+ }
}
// ─── Helpers ────────────────────────────────────────────────────────────────
diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts
index 04da509..8533dd8 100644
--- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts
+++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts
@@ -73,6 +73,16 @@ declare class RebreakProtectionModule extends NativeModule;
+ /**
+ * iOS: liest die nativen Protection-Logs (SharedLogStore — NEFilter/
+ * FamilyControls Flow) aus dem App-Group-UserDefaults. Für Debug-Page,
+ * damit TestFlight-Tester den nativen Flow ohne Mac/Console.app sehen.
+ */
+ getProtectionLogs(): Promise;
+
+ /** iOS: leert die nativen Protection-Logs. */
+ clearProtectionLogs(): Promise;
+
// ─── Android-spezifische Methoden (auf iOS undefined zur Laufzeit) ───────
/** Android: Live-Check ob unser AccessibilityService aktuell als enabled