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