## Protection Pre-Explainer: External Pointer Vorher: Pulse-Ring absolute-positioniert IM Screenshot — Position musste per-locale fine-tuned werden weil Apple-Dialog-Höhe variiert (DE/EN/FR/AR haben unterschiedliche Text-Längen → Dialog hat verschiedene Höhen → Erlauben-Button rutscht). Jetzt: animierter Pfeil + Label-Pill UNTER dem Screenshot. Dimensions- agnostic, funktioniert in allen 4 Sprachen ohne Locale-spezifische Magie. - ScreenshotPointer komplett refactored: caret-up + bouncing pill mit Button-Label-Text (z.B. 'Tippe "Erlauben"' / 'Tap "Allow"' / etc.) - onboardingAssets.ts: getPointerPosition deprecated/entfernt - ProtectionSlide nutzt neue API mit buttonLabelKey - 4 Locales: dialog_button_allow + dialog_button_continue - tap_marker_hint refined (kein "roter Marker"-Ref mehr) ## i18n-aware Screenshots en/fr/ar Permission-Dialog-Screenshots zur Map ergänzt. Resolver fällt auf de zurück wenn andere Sprache fehlt. ## Dynamic Sizing ProtectionSlide nutzt useWindowDimensions: height: min(320, max(200, screenH * 0.32)) → passt auf iPhone SE (213px) bis Pro Max (320px capped) ohne Scroll. OnboardingShell ScrollView-Padding reduziert (16→12 top, 24→16 bottom). ProtectionSlide-Spacing tightened. ## Blocker: lockedIn Fix Bug: `lockedIn = appDeletionLockActive` ignorierte URL-Filter-State — wenn User nur FC aktivierte (ohne URL-Filter), zeigte App grünen "Schutz aktiv"-Banner obwohl URL-Filter aus war. Fix: lockedIn = urlFilter && appDeletionLock → Beide müssen wirklich aktiv sein für den grünen Banner. ## LayerSwitchCard: lockedHint Prop Optional Hint-Text der unter dem active Layer angezeigt wird, z.B. "System-gesperrt. Nur in iOS-Einstellungen → Bildschirmzeit → Verwaltung durch ReBreak deaktivierbar.". Wird für iOS App-Lock-Card genutzt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
144 lines
4.1 KiB
TypeScript
144 lines
4.1 KiB
TypeScript
import { useState } from 'react';
|
|
import { View, Text, Switch, ActivityIndicator } from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
|
|
type Props = {
|
|
icon: React.ComponentProps<typeof Ionicons>['name'];
|
|
title: string;
|
|
subtitle: string;
|
|
/** Wenn true: zeigt grünen Check statt Switch (Layer ist an). */
|
|
active: boolean;
|
|
/** Aktivierung (zeigt System-Dialog). UI hat nur read-on-flow,
|
|
* Toggle-off ist nicht hier — passiert nur über Cooldown. */
|
|
onActivate: () => Promise<{ enabled: boolean; error?: string }>;
|
|
/** Optional: Hinweistext unter Subtitle. Wird OBEN angezeigt wenn inactive
|
|
* (warnung vor Aktivierung) UND UNTEN wenn active+lockedHint (Hinweis
|
|
* wo man's wieder deaktiviert — bei Family-Controls/Screen-Time z.B.). */
|
|
warning?: string;
|
|
/** Optional: Hinweis-Text wenn active=true (z.B. "Nur in iOS-Settings
|
|
* deaktivierbar"). Macht die System-managed-Natur klar. */
|
|
lockedHint?: string;
|
|
};
|
|
|
|
export function LayerSwitchCard({ icon, title, subtitle, active, onActivate, warning, lockedHint }: Props) {
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handleSwitch(v: boolean) {
|
|
if (!v || active || busy) return;
|
|
setBusy(true);
|
|
try {
|
|
await onActivate();
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
const iconBg = active ? '#dcfce7' : '#f5f5f5';
|
|
const iconColor = active ? '#16a34a' : '#737373';
|
|
const borderColor = active ? '#86efac' : '#e5e5e5';
|
|
const cardBg = active ? '#f0fdf4' : '#ffffff';
|
|
|
|
return (
|
|
<View
|
|
style={{
|
|
backgroundColor: cardBg,
|
|
borderWidth: 1,
|
|
borderColor,
|
|
borderRadius: 16,
|
|
padding: 14,
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
<View
|
|
style={{
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 12,
|
|
backgroundColor: iconBg,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Ionicons name={icon} size={20} color={iconColor} />
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
|
{title}
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
fontSize: 12,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: '#525252',
|
|
marginTop: 2,
|
|
}}
|
|
>
|
|
{subtitle}
|
|
</Text>
|
|
</View>
|
|
|
|
{busy ? (
|
|
<ActivityIndicator color={iconColor} />
|
|
) : active ? (
|
|
<Ionicons name="checkmark-circle" size={28} color="#16a34a" />
|
|
) : (
|
|
<Switch value={false} onValueChange={handleSwitch} trackColor={{ true: '#16a34a' }} />
|
|
)}
|
|
</View>
|
|
|
|
{warning && !active && (
|
|
<View
|
|
style={{
|
|
marginTop: 10,
|
|
paddingTop: 10,
|
|
borderTopWidth: 1,
|
|
borderTopColor: 'rgba(0,0,0,0.06)',
|
|
flexDirection: 'row',
|
|
gap: 8,
|
|
}}
|
|
>
|
|
<Ionicons name="information-circle" size={16} color="#525252" />
|
|
<Text
|
|
style={{
|
|
flex: 1,
|
|
fontSize: 11,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: '#525252',
|
|
lineHeight: 16,
|
|
}}
|
|
>
|
|
{warning}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{lockedHint && active && (
|
|
<View
|
|
style={{
|
|
marginTop: 10,
|
|
paddingTop: 10,
|
|
borderTopWidth: 1,
|
|
borderTopColor: 'rgba(0,0,0,0.06)',
|
|
flexDirection: 'row',
|
|
gap: 8,
|
|
alignItems: 'flex-start',
|
|
}}
|
|
>
|
|
<Ionicons name="lock-closed" size={14} color="#16a34a" style={{ marginTop: 1 }} />
|
|
<Text
|
|
style={{
|
|
flex: 1,
|
|
fontSize: 11,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: '#16a34a',
|
|
lineHeight: 16,
|
|
}}
|
|
>
|
|
{lockedHint}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|