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>
190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Image, Text, TouchableOpacity, View } from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useColors } from '../../lib/theme';
|
|
import { FormSheet } from '../FormSheet';
|
|
|
|
/**
|
|
* Anti-Blind-Klick-Gate fürs Onboarding: zeigt VOR der eigentlichen Permission
|
|
* eine kurze Instruktion (welcher Button im gleich folgenden System-Dialog) und
|
|
* verlangt ein bewusstes Häkchen, bevor der „Weiter"-Button aktiv wird. Bricht
|
|
* das „weiter-weiter"-Muster genau dort, wo User sonst den falschen Button tippen
|
|
* (v.a. iOS-Family-Controls: zwei Buttons, der blaue ist die Falle).
|
|
*
|
|
* Onboarding-only — der Blocker-Screen aktiviert weiterhin direkt (kein Gate).
|
|
*/
|
|
export function PermissionConfirmSheet({
|
|
visible,
|
|
title,
|
|
body,
|
|
steps,
|
|
screenshot,
|
|
indicatorCaption,
|
|
onConfirm,
|
|
onClose,
|
|
}: {
|
|
visible: boolean;
|
|
title: string;
|
|
body: string;
|
|
/** Optional: nummerierte Schritte (z.B. a11y: erst Overlay erlauben, dann Schalter an). */
|
|
steps?: string[];
|
|
/** Optional: Screenshot (require-Handle) — zeigt dem User wie der Ziel-Screen aussieht. */
|
|
screenshot?: number;
|
|
/** Optional: Caption unter dem Screenshot — der Indikator „hier tippen". */
|
|
indicatorCaption?: string;
|
|
onConfirm: () => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const colors = useColors();
|
|
const [checked, setChecked] = useState(false);
|
|
|
|
// Häkchen bei jedem Öffnen zurücksetzen — sonst klickt man beim nächsten Step
|
|
// mit schon-gesetztem Haken blind durch (genau das wollen wir verhindern).
|
|
useEffect(() => {
|
|
if (visible) setChecked(false);
|
|
}, [visible]);
|
|
|
|
return (
|
|
<FormSheet visible={visible} onClose={onClose} title={title}>
|
|
{/* FormSheet padded nur den Titel, nicht die children → hier selbst polstern. */}
|
|
<View style={{ paddingHorizontal: 20, paddingTop: 4, paddingBottom: 28 }}>
|
|
<View
|
|
style={{
|
|
backgroundColor: colors.surfaceElevated,
|
|
borderRadius: 14,
|
|
padding: 16,
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
}}
|
|
>
|
|
<Ionicons name="information-circle" size={22} color={colors.brandOrange} style={{ marginTop: 1 }} />
|
|
<Text
|
|
style={{
|
|
flex: 1,
|
|
fontSize: 15,
|
|
lineHeight: 22,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
{body}
|
|
</Text>
|
|
</View>
|
|
|
|
{steps && steps.length > 0 && (
|
|
<View style={{ marginTop: 12, gap: 8 }}>
|
|
{steps.map((step, i) => (
|
|
<View key={i} style={{ flexDirection: 'row', alignItems: 'flex-start', gap: 10 }}>
|
|
<View
|
|
style={{
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: 11,
|
|
backgroundColor: colors.brandOrange,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginTop: 1,
|
|
}}
|
|
>
|
|
<Text style={{ fontSize: 12, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{i + 1}</Text>
|
|
</View>
|
|
<Text
|
|
style={{
|
|
flex: 1,
|
|
fontSize: 14,
|
|
lineHeight: 20,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
{step}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{screenshot != null && (
|
|
<View style={{ marginTop: 14, alignItems: 'center' }}>
|
|
<View
|
|
style={{
|
|
height: 240,
|
|
aspectRatio: 0.9,
|
|
borderRadius: 12,
|
|
overflow: 'hidden',
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
}}
|
|
>
|
|
<Image source={screenshot} style={{ width: '100%', height: '100%' }} resizeMode="contain" />
|
|
</View>
|
|
{!!indicatorCaption && (
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 8 }}>
|
|
<Ionicons name="arrow-up" size={16} color={colors.brandOrange} />
|
|
<Text
|
|
style={{
|
|
fontSize: 13,
|
|
fontFamily: 'Nunito_700Bold',
|
|
color: colors.brandOrange,
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
{indicatorCaption}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
<TouchableOpacity
|
|
activeOpacity={0.7}
|
|
onPress={() => setChecked((c) => !c)}
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
marginTop: 20,
|
|
paddingVertical: 4,
|
|
}}
|
|
>
|
|
<Ionicons
|
|
name={checked ? 'checkbox' : 'square-outline'}
|
|
size={26}
|
|
color={checked ? colors.brandOrange : colors.textMuted}
|
|
/>
|
|
<Text
|
|
style={{
|
|
flex: 1,
|
|
fontSize: 15,
|
|
fontFamily: 'Nunito_700Bold',
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
{t('onboarding.protection_confirm.checkbox')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
disabled={!checked}
|
|
activeOpacity={0.85}
|
|
onPress={onConfirm}
|
|
style={{
|
|
backgroundColor: colors.brandOrange,
|
|
borderRadius: 12,
|
|
paddingVertical: 15,
|
|
alignItems: 'center',
|
|
marginTop: 18,
|
|
opacity: checked ? 1 : 0.4,
|
|
}}
|
|
>
|
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#fff' }}>
|
|
{t('onboarding.protection_confirm.cta')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</FormSheet>
|
|
);
|
|
}
|