chahinebrini 4a013bc43b feat(android-protection): präzise Tamper-Lock + a11y-Onboarding-Guide
Tamper-Lock von Keyword-Scanning auf präzise Einzel-Surfaces umgebaut:
blockt nur ReBreaks eigene Screens (Admin-Deaktivierung via DeviceAdminAdd,
a11y-Ausschalten, VPN-Trennen/Surface), nie Listen oder fremde Apps.

- Deny-Removal = Admin-only: OS graut Uninstall+Force-Stop für aktiven
  Device-Admin aus; einziger Bypass (Admin deaktivieren) bleibt a11y-gesperrt.
  Andere Apps verwalten/force-stoppen/deinstallieren bleibt komplett frei.
- a11y-Onboarding: passiver Bottom-Overlay-Hinweis + Settings-Reset auf
  Startseite nach Aktivierung + 1s-Delay vor App-Rückkehr.
- VPN-Trennen-Dialog + a11y-Ausschalten neu abgedeckt.
- a11y-Service-Icon im Plugin (klar als ReBreak erkennbar).

Verifiziert auf A50 per logcat: alle 4 Surfaces blocken, Listen + fremde
Apps frei, keine False-Positives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 04:05:41 +02:00

545 lines
18 KiB
TypeScript

import { useState, type ReactNode } from "react";
import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native";
import { Ionicons } from "@expo/vector-icons";
// Geteilter Schutz-Setup-Flow (Reihenfolge + Gating). Quelle der Wahrheit für
// Blocker UND Onboarding — NIE die Reihenfolge hier ändern ohne beide zu prüfen.
// Android: VPN → Geräteadmin → a11y (a11y zuletzt, sonst blockt der Tamper-Lock
// die Admin-Seite). iOS: App-Lock → Bildschirmzeit → URL-Filter.
// ─── iOS Unsupervised 3-Step Setup Flow ──────────────────────────────────────
type SetupFlowProps = {
familyControlsActive: boolean;
screentimeCode: string | null;
screentimeConfirmed: boolean;
screentimeSaving: boolean;
urlFilterActive: boolean;
onActivateFamilyControls: () => Promise<{ enabled: boolean; error?: string }>;
onGenerateScreentimeCode: () => void;
onConfirmScreentime: () => void;
onActivateUrlFilter: () => Promise<{ enabled: boolean; error?: string }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
};
export function IosUnsupervisedSetupFlow({
familyControlsActive,
screentimeCode,
screentimeConfirmed,
screentimeSaving,
urlFilterActive,
onActivateFamilyControls,
onGenerateScreentimeCode,
onConfirmScreentime,
onActivateUrlFilter,
colors,
t,
}: SetupFlowProps) {
const step1Done = familyControlsActive;
const step2Done = screentimeConfirmed;
const step3Done = urlFilterActive;
return (
<View style={{ gap: 10 }}>
<SetupStep1
done={step1Done}
onActivate={onActivateFamilyControls}
colors={colors}
t={t}
/>
<SetupStep2
unlocked={step1Done}
code={screentimeCode}
confirmed={step2Done}
saving={screentimeSaving}
onGenerate={onGenerateScreentimeCode}
onConfirm={onConfirmScreentime}
colors={colors}
t={t}
/>
<SetupStep3
unlocked={step2Done}
done={step3Done}
onActivate={onActivateUrlFilter}
colors={colors}
t={t}
/>
</View>
);
}
function SetupStep1({
done,
onActivate,
colors,
t,
}: {
done: boolean;
onActivate: () => Promise<{ enabled: boolean; error?: string }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
}) {
const [busy, setBusy] = useState(false);
async function handlePress() {
if (done || busy) return;
setBusy(true);
try { await onActivate(); } finally { setBusy(false); }
}
return (
<SetupStepCard
stepNumber={1}
title={t('blocker.setup_step1_title')}
subtitle={done ? t('blocker.setup_step1_subtitle_done') : t('blocker.setup_step1_subtitle_pending')}
done={done}
unlocked
colors={colors}
>
{!done && (
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.8}
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center', marginTop: 10 }}
>
{busy
? <ActivityIndicator size="small" color="#fff" />
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.setup_step1_cta')}</Text>
}
</TouchableOpacity>
)}
</SetupStepCard>
);
}
function SetupStep2({
unlocked,
code,
confirmed,
saving,
onGenerate,
onConfirm,
colors,
t,
}: {
unlocked: boolean;
code: string | null;
confirmed: boolean;
saving: boolean;
onGenerate: () => void;
onConfirm: () => void;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
}) {
return (
<SetupStepCard
stepNumber={2}
title={t('blocker.setup_step2_title')}
subtitle={confirmed ? t('blocker.setup_step2_subtitle_done') : t('blocker.setup_step2_subtitle_pending')}
done={confirmed}
unlocked={unlocked}
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 1 })}
colors={colors}
>
{unlocked && !confirmed && (
<View style={{ gap: 10, marginTop: 10 }}>
{!code ? (
<TouchableOpacity
onPress={onGenerate}
activeOpacity={0.8}
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center' }}
>
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>
{t('blocker.screentime_generate_cta')}
</Text>
</TouchableOpacity>
) : (
<View style={{ gap: 10 }}>
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 12, padding: 16, alignItems: 'center', gap: 4 }}>
<Text style={{ fontSize: 11, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted }}>
{t('blocker.screentime_code_label')}
</Text>
<Text style={{ fontSize: 36, fontFamily: 'Nunito_700Bold', color: colors.brandOrange, letterSpacing: 12 }}>
{code}
</Text>
</View>
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 12, padding: 12, gap: 6 }}>
{[
t('blocker.screentime_step1'),
t('blocker.screentime_step2'),
t('blocker.screentime_step3'),
].map((step, i) => (
<Text
key={i}
style={{
fontSize: 13,
fontFamily: i === 0 ? 'Nunito_700Bold' : 'Nunito_600SemiBold',
color: i === 0 ? colors.brandOrange : colors.text,
lineHeight: 18,
}}
>
{step}
</Text>
))}
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 16, marginTop: 2 }}>
{t('blocker.screentime_step_note')}
</Text>
</View>
<TouchableOpacity
onPress={onConfirm}
disabled={saving}
activeOpacity={0.8}
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center' }}
>
{saving
? <ActivityIndicator size="small" color="#fff" />
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.screentime_confirm_cta')}</Text>
}
</TouchableOpacity>
</View>
)}
</View>
)}
</SetupStepCard>
);
}
function SetupStep3({
unlocked,
done,
onActivate,
colors,
t,
}: {
unlocked: boolean;
done: boolean;
onActivate: () => Promise<{ enabled: boolean; error?: string }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
}) {
const [busy, setBusy] = useState(false);
async function handlePress() {
if (done || busy) return;
setBusy(true);
try { await onActivate(); } finally { setBusy(false); }
}
return (
<SetupStepCard
stepNumber={3}
title={t('blocker.setup_step3_title')}
subtitle={done ? t('blocker.setup_step3_subtitle_done') : t('blocker.setup_step3_subtitle_pending')}
done={done}
unlocked={unlocked}
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 2 })}
colors={colors}
>
{unlocked && !done && (
<View style={{ gap: 10, marginTop: 10 }}>
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 10, padding: 10, flexDirection: 'row', gap: 8 }}>
<Ionicons name="information-circle-outline" size={15} color={colors.textMuted} style={{ marginTop: 1 }} />
<Text style={{ flex: 1, fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 17 }}>
{t('blocker.setup_step3_warning')}
</Text>
</View>
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.8}
style={{ backgroundColor: colors.success, borderRadius: 10, paddingVertical: 10, alignItems: 'center' }}
>
{busy
? <ActivityIndicator size="small" color="#fff" />
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.setup_step3_cta')}</Text>
}
</TouchableOpacity>
</View>
)}
</SetupStepCard>
);
}
// ─── Android 3-Step Setup Flow ───────────────────────────────────────────────
type AndroidSetupFlowProps = {
vpnActive: boolean;
// = a11y-Service enabled UND Tamper-Lock armed (appDeletionLock). Nur "enabled"
// reicht NICHT — der a11y-Service ist ohne armed-Flag komplett passiv.
accessibilityLocked: boolean;
deviceAdminActive: boolean;
onActivateVpn: () => Promise<{ enabled: boolean; error?: string }>;
onActivateAccessibility: () => Promise<unknown>;
onRequestDeviceAdmin: () => Promise<{ launched: boolean }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
};
export function AndroidSetupFlow({
vpnActive,
accessibilityLocked,
deviceAdminActive,
onActivateVpn,
onActivateAccessibility,
onRequestDeviceAdmin,
colors,
t,
}: AndroidSetupFlowProps) {
// Reihenfolge KRITISCH: VPN → Geräteadmin → a11y. a11y MUSS zuletzt, weil der
// Tamper-Lock (sobald armed) die Geräteadmin-Seite blockt — sonst kann der
// User den Admin gar nicht mehr aktivieren.
const vpnDone = vpnActive;
const adminDone = deviceAdminActive;
const a11yDone = accessibilityLocked;
return (
<View style={{ gap: 10 }}>
<AndroidStep1
done={vpnDone}
onActivate={onActivateVpn}
colors={colors}
t={t}
/>
{/* Display-Step 2 = Geräteadmin (Komponente AndroidStep3, i18n android_step3_*). */}
<AndroidStep3
unlocked={vpnDone}
done={adminDone}
onRequestAdmin={onRequestDeviceAdmin}
colors={colors}
t={t}
/>
{/* Display-Step 3 = a11y / ReBreak-Schutz (Komponente AndroidStep2, i18n android_step2_*). */}
<AndroidStep2
unlocked={adminDone}
done={a11yDone}
onActivate={onActivateAccessibility}
colors={colors}
t={t}
/>
</View>
);
}
function AndroidStep1({
done,
onActivate,
colors,
t,
}: {
done: boolean;
onActivate: () => Promise<{ enabled: boolean; error?: string }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
}) {
const [busy, setBusy] = useState(false);
async function handlePress() {
if (done || busy) return;
setBusy(true);
try { await onActivate(); } finally { setBusy(false); }
}
return (
<SetupStepCard
stepNumber={1}
title={t('blocker.android_step1_title')}
subtitle={done ? t('blocker.android_step1_subtitle_done') : t('blocker.android_step1_subtitle_pending')}
done={done}
unlocked
colors={colors}
>
{!done && (
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.8}
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center', marginTop: 10 }}
>
{busy
? <ActivityIndicator size="small" color="#fff" />
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_step1_cta')}</Text>
}
</TouchableOpacity>
)}
</SetupStepCard>
);
}
function AndroidStep2({
unlocked,
done,
onActivate,
colors,
t,
}: {
unlocked: boolean;
done: boolean;
onActivate: () => Promise<unknown>;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
}) {
const [busy, setBusy] = useState(false);
async function handlePress() {
if (busy) return;
setBusy(true);
try { await onActivate(); } finally { setBusy(false); }
}
return (
<SetupStepCard
stepNumber={3}
title={t('blocker.android_step2_title')}
subtitle={done ? t('blocker.android_step2_subtitle_done') : t('blocker.android_step2_subtitle_pending')}
done={done}
unlocked={unlocked}
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 2 })}
colors={colors}
>
{unlocked && !done && (
<View style={{ gap: 8, marginTop: 10 }}>
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 12, padding: 12, gap: 6 }}>
{[
t('blocker.android_step2_instruction1'),
t('blocker.android_step2_instruction2'),
t('blocker.android_step2_instruction3'),
].map((line, i) => (
<Text
key={i}
style={{
fontSize: 13,
fontFamily: i === 0 ? 'Nunito_700Bold' : 'Nunito_600SemiBold',
color: i === 0 ? colors.brandOrange : colors.text,
lineHeight: 18,
}}
>
{line}
</Text>
))}
</View>
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.8}
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center', marginTop: 2 }}
>
{busy
? <ActivityIndicator size="small" color="#fff" />
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_step2_cta')}</Text>
}
</TouchableOpacity>
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 16 }}>
{t('blocker.android_step2_note')}
</Text>
</View>
)}
</SetupStepCard>
);
}
function AndroidStep3({
unlocked,
done,
onRequestAdmin,
colors,
t,
}: {
unlocked: boolean;
done: boolean;
onRequestAdmin: () => Promise<{ launched: boolean }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
}) {
const [busy, setBusy] = useState(false);
async function handlePress() {
if (done || busy) return;
setBusy(true);
try { await onRequestAdmin(); } finally { setBusy(false); }
}
return (
<SetupStepCard
stepNumber={2}
title={t('blocker.android_step3_title')}
subtitle={done ? t('blocker.android_step3_subtitle_done') : t('blocker.android_step3_subtitle_pending')}
done={done}
unlocked={unlocked}
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 1 })}
colors={colors}
>
{unlocked && !done && (
<View style={{ gap: 10, marginTop: 10 }}>
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 10, padding: 10, flexDirection: 'row', gap: 8 }}>
<Ionicons name="information-circle-outline" size={15} color={colors.textMuted} style={{ marginTop: 1 }} />
<Text style={{ flex: 1, fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 17 }}>
{t('blocker.android_step3_warning')}
</Text>
</View>
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.8}
style={{ backgroundColor: colors.success, borderRadius: 10, paddingVertical: 10, alignItems: 'center' }}
>
{busy
? <ActivityIndicator size="small" color="#fff" />
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_step3_cta')}</Text>
}
</TouchableOpacity>
</View>
)}
</SetupStepCard>
);
}
function SetupStepCard({
stepNumber,
title,
subtitle,
done,
unlocked,
lockedHint,
colors,
children,
}: {
stepNumber: number;
title: string;
subtitle: string;
done: boolean;
unlocked: boolean;
lockedHint?: string;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
children?: ReactNode;
}) {
const borderColor = done ? '#86efac' : unlocked ? colors.border : colors.border;
const cardBg = done ? '#f0fdf4' : colors.surface;
const numberBg = done ? '#dcfce7' : unlocked ? colors.surfaceElevated : colors.surfaceElevated;
const numberColor = done ? colors.success : unlocked ? colors.text : colors.textMuted;
const titleColor = done ? colors.success : unlocked ? colors.text : colors.textMuted;
return (
<View style={{ backgroundColor: cardBg, borderWidth: 1, borderColor, borderRadius: 16, padding: 14 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
<View style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: numberBg, alignItems: 'center', justifyContent: 'center' }}>
{done
? <Ionicons name="checkmark" size={18} color={colors.success} />
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: numberColor }}>{stepNumber}</Text>
}
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: titleColor }}>{title}</Text>
{!!subtitle && (
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, marginTop: 2, lineHeight: 17 }}>{subtitle}</Text>
)}
</View>
{done && <Ionicons name="checkmark-circle" size={24} color={colors.success} />}
</View>
{lockedHint && !done && (
<View style={{ marginTop: 8, flexDirection: 'row', gap: 6, alignItems: 'center' }}>
<Ionicons name="lock-closed-outline" size={13} color={colors.textMuted} />
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>{lockedHint}</Text>
</View>
)}
{children}
</View>
);
}