fix(protection): gate Family Controls on entitlement + native log viewer
Verifiziert via Build-11-.ipa-Inspektion + Device-Logs: - com.apple.developer.family-controls FEHLT im signierten Distribution-Build (Plugin gated korrekt auf REBREAK_ENABLE_FAMILY_CONTROLS — Apple- Distribution-Approval pending). - Device-Log beweist: ManagedSettingsAgent-XPC → "error 159 Sandbox restriction" → NSCocoaErrorDomain:4099. Kein Daemon-/Race-Problem, Retry zwecklos. Root-Cause des FC-Bugs: app.config.ts hatte familyControlsEnabled hart auf true (falscher "approved"-Kommentar) → App bot App-Lock an obwohl sandbox-blockiert. FAMILY_CONTROLS_AVAILABLE wurde nirgends konsumiert. Fixes: - app.config.ts: familyControlsEnabled an REBREAK_ENABLE_FAMILY_CONTROLS gekoppelt (== Plugin-Gating) → in TestFlight/production false. - blocker.tsx: iOS App-Lock-Card nur wenn FAMILY_CONTROLS_AVAILABLE. lockedIn akzeptiert URL-Filter allein wenn FC build-seitig fehlt. - ProtectionSlide.tsx: Onboarding überspringt den App-Lock-Step (FC) wenn nicht verfügbar — URL-Filter allein = vollwertiger Schutz. Native Log-Viewer (für NEFilter-Debug ohne Mac/Console.app): - Swift: getProtectionLogs/clearProtectionLogs lesen SharedLogStore aus App-Group-UserDefaults. - lib/protection.ts wrapper + TS-module-types. - debug.tsx: ProtectionLogCard (iOS) — native NEFilter/FC-Logs in der App sichtbar, copy/clear/refresh.
This commit is contained in:
parent
b8e4b02b88
commit
f318364c7e
@ -131,12 +131,19 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Family Controls Entitlement (denyAppRemoval via ManagedSettings) ist seit
|
// Family Controls (denyAppRemoval via ManagedSettings) braucht ein
|
||||||
// 2026-05 für ReBreak via Apple-Entitlement-Request approved und in TestFlight-
|
// DISTRIBUTION-Entitlement das noch bei Apple zur Freigabe liegt. Ohne das
|
||||||
// sowie production-Builds aktiv. Daher hart auf `true` — keine Build-Flag-Gating
|
// Entitlement blockt iOS den ManagedSettingsAgent-XPC auf Sandbox-Ebene
|
||||||
// mehr nötig. Legacy `REBREAK_ENABLE_FAMILY_CONTROLS=1` aus dev-builds wird
|
// (NSCocoaErrorDomain:4099 "Sandbox restriction" — verifiziert in Build 11).
|
||||||
// ignoriert (das war Übergangs-Gating vor Apple-Approval).
|
//
|
||||||
familyControlsEnabled: true,
|
// 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:
|
apiUrl:
|
||||||
process.env.EXPO_PUBLIC_API_URL ||
|
process.env.EXPO_PUBLIC_API_URL ||
|
||||||
process.env.API_URL ||
|
process.env.API_URL ||
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { useProtectionState } from '../../hooks/useProtectionState';
|
|||||||
import { useCustomDomains } from '../../hooks/useCustomDomains';
|
import { useCustomDomains } from '../../hooks/useCustomDomains';
|
||||||
import { useBlocklistSync } from '../../hooks/useBlocklistSync';
|
import { useBlocklistSync } from '../../hooks/useBlocklistSync';
|
||||||
import { useDomainSubmissionRealtime } from '../../hooks/useDomainSubmissionRealtime';
|
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';
|
import { useColors, type ColorScheme } from '../../lib/theme';
|
||||||
|
|
||||||
export default function BlockerScreen() {
|
export default function BlockerScreen() {
|
||||||
@ -79,7 +79,11 @@ export default function BlockerScreen() {
|
|||||||
// (Hardening). Family-Controls ALLEINE = kein Schutz, nur denyAppRemoval —
|
// (Hardening). Family-Controls ALLEINE = kein Schutz, nur denyAppRemoval —
|
||||||
// ohne URL-Filter sieht der User trotzdem Glücksspielseiten. Daher BEIDE
|
// ohne URL-Filter sieht der User trotzdem Glücksspielseiten. Daher BEIDE
|
||||||
// müssen an sein damit der "Schutz aktiv"-Banner gezeigt wird.
|
// 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);
|
const urlFilterActiveRef = useRef(urlFilterActive);
|
||||||
useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]);
|
useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]);
|
||||||
@ -275,7 +279,11 @@ export default function BlockerScreen() {
|
|||||||
onActivate={handleActivateFamilyControls}
|
onActivate={handleActivateFamilyControls}
|
||||||
warning={t('blocker.layers_app_lock_warning')}
|
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). */
|
||||||
<LayerSwitchCard
|
<LayerSwitchCard
|
||||||
icon="lock-closed-outline"
|
icon="lock-closed-outline"
|
||||||
title={t('blocker.layers_app_lock_title')}
|
title={t('blocker.layers_app_lock_title')}
|
||||||
@ -289,7 +297,7 @@ export default function BlockerScreen() {
|
|||||||
warning={t('blocker.layers_app_lock_warning')}
|
warning={t('blocker.layers_app_lock_warning')}
|
||||||
lockedHint={t('blocker.layers_app_lock_locked_hint')}
|
lockedHint={t('blocker.layers_app_lock_locked_hint')}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -120,6 +120,7 @@ export default function DebugScreen() {
|
|||||||
|
|
||||||
<RealtimeStatusCard />
|
<RealtimeStatusCard />
|
||||||
<RealtimeLogCard />
|
<RealtimeLogCard />
|
||||||
|
{Platform.OS === 'ios' ? <ProtectionLogCard /> : null}
|
||||||
|
|
||||||
<DebugStub
|
<DebugStub
|
||||||
title="LLM-Provider Toggle"
|
title="LLM-Provider Toggle"
|
||||||
@ -460,6 +461,130 @@ function RealtimeLogCard() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Protection Log Card (iOS native NEFilter/FamilyControls flow) ──────────
|
||||||
|
|
||||||
|
function ProtectionLogCard() {
|
||||||
|
const colors = useColors();
|
||||||
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
|
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 (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
borderRadius: 14,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(0,0,0,0.05)',
|
||||||
|
padding: 14,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 11,
|
||||||
|
backgroundColor: colors.surfaceElevated,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="shield-outline" size={18} color={colors.textMuted} />
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_700Bold', flex: 1 }}>
|
||||||
|
Protection Log (nativ)
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity onPress={loadLogs} hitSlop={8} activeOpacity={0.6} style={{ marginRight: 8 }}>
|
||||||
|
<Ionicons name="refresh-outline" size={17} color={colors.textMuted} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={copyLogs} hitSlop={8} activeOpacity={0.6} style={{ marginRight: 8 }}>
|
||||||
|
<Ionicons name="copy-outline" size={17} color={colors.textMuted} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={clearLogs} hitSlop={8} activeOpacity={0.6}>
|
||||||
|
<Ionicons name="trash-outline" size={17} color={colors.textMuted} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.textMuted,
|
||||||
|
fontFamily: 'Nunito_400Regular',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
paddingVertical: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading
|
||||||
|
? 'Lade...'
|
||||||
|
: 'Keine Logs — aktiviere den URL-Filter, dann auf Refresh tippen.'}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<View style={{ maxHeight: 320, overflow: 'hidden' }}>
|
||||||
|
<ScrollView style={{ flex: 1 }} nestedScrollEnabled showsVerticalScrollIndicator>
|
||||||
|
{logs.map((line, i) => (
|
||||||
|
<Text
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
lineHeight: 15,
|
||||||
|
color: line.includes('❌') || line.includes('failed')
|
||||||
|
? '#dc2626'
|
||||||
|
: line.includes('✅')
|
||||||
|
? '#16a34a'
|
||||||
|
: colors.textMuted,
|
||||||
|
fontFamily: 'Menlo',
|
||||||
|
marginBottom: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
color: colors.textMuted,
|
||||||
|
fontFamily: 'Nunito_400Regular',
|
||||||
|
marginTop: 8,
|
||||||
|
opacity: 0.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
SharedLogStore — max 200 Einträge, neueste oben
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function LogLine({
|
function LogLine({
|
||||||
entry,
|
entry,
|
||||||
colors,
|
colors,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useColors } from '../../../lib/theme';
|
import { useColors } from '../../../lib/theme';
|
||||||
import { apiFetch } from '../../../lib/api';
|
import { apiFetch } from '../../../lib/api';
|
||||||
import { invalidateMe } from '../../../hooks/useMe';
|
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 RebreakProtection from '../../../modules/rebreak-protection';
|
||||||
import { getPermissionScreenshot } from '../../../lib/onboardingAssets';
|
import { getPermissionScreenshot } from '../../../lib/onboardingAssets';
|
||||||
import { OnboardingShell } from '../OnboardingShell';
|
import { OnboardingShell } from '../OnboardingShell';
|
||||||
@ -76,6 +76,14 @@ function IosProtectionSlide({
|
|||||||
);
|
);
|
||||||
return;
|
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');
|
setPhase('preexplain_lock');
|
||||||
} finally {
|
} finally {
|
||||||
setActivating(false);
|
setActivating(false);
|
||||||
@ -147,7 +155,13 @@ function IosProtectionSlide({
|
|||||||
onClose={() => setPermissionDeniedOpen(false)}
|
onClose={() => setPermissionDeniedOpen(false)}
|
||||||
onRetry={async () => {
|
onRetry={async () => {
|
||||||
const res = await protection.resetUrlFilter();
|
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;
|
return res;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -214,6 +214,27 @@ export const protection = {
|
|||||||
return RebreakProtection.openSystemSettings(target);
|
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<string[]> {
|
||||||
|
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<void> {
|
||||||
|
if (Platform.OS !== "ios") return;
|
||||||
|
try {
|
||||||
|
await RebreakProtection.clearProtectionLogs();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
addLayerChangeListener(cb: (layers: DeviceLayers) => void) {
|
addLayerChangeListener(cb: (layers: DeviceLayers) => void) {
|
||||||
return RebreakProtection.addListener("onLayerChange", cb);
|
return RebreakProtection.addListener("onLayerChange", cb);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 ────────────────────────────────────────────────────────────────
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -73,6 +73,16 @@ declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEven
|
|||||||
/** Öffnet System-Settings auf dem entsprechenden Tab. */
|
/** Öffnet System-Settings auf dem entsprechenden Tab. */
|
||||||
openSystemSettings(target?: SystemSettingsTarget): Promise<void>;
|
openSystemSettings(target?: SystemSettingsTarget): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string[]>;
|
||||||
|
|
||||||
|
/** iOS: leert die nativen Protection-Logs. */
|
||||||
|
clearProtectionLogs(): Promise<void>;
|
||||||
|
|
||||||
// ─── Android-spezifische Methoden (auf iOS undefined zur Laufzeit) ───────
|
// ─── Android-spezifische Methoden (auf iOS undefined zur Laufzeit) ───────
|
||||||
|
|
||||||
/** Android: Live-Check ob unser AccessibilityService aktuell als enabled
|
/** Android: Live-Check ob unser AccessibilityService aktuell als enabled
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user