## 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>
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Animated, Easing, Text, View } from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useColors } from '../../lib/theme';
|
|
|
|
/**
|
|
* "Tap-Here"-Indicator UNTER einem Screenshot.
|
|
*
|
|
* Vorher: absolut-positionierter Pulse-Ring INNERHALB des Screenshots
|
|
* → musste per-locale fine-tuned werden weil Apple-Dialog-Dimensionen
|
|
* pro Sprache variieren (DE-Text länger als EN, Modal höher, Button
|
|
* rutscht runter…).
|
|
*
|
|
* Jetzt: render-unter dem Screenshot. Layout-agnostic. Animation lenkt
|
|
* Aufmerksamkeit auf die richtige Region ohne pixel-genaue Position.
|
|
*
|
|
* ┌──────────────────────────┐
|
|
* │ <iOS Dialog Screenshot> │
|
|
* │ [Nicht erlauben] │
|
|
* │ [Erlauben] │
|
|
* └──────────────────────────┘
|
|
* ▲
|
|
* ┌─────────────┐
|
|
* │ ▲ Tippe │ ← Animated bouncing pill mit Pfeil + Label
|
|
* │ "Erlauben" │
|
|
* └─────────────┘
|
|
*/
|
|
export function ScreenshotPointer({
|
|
buttonLabel,
|
|
}: {
|
|
/** Der Text auf dem korrekten Button im iOS-Dialog. Wird ins Label übernommen. */
|
|
buttonLabel: string;
|
|
}) {
|
|
const colors = useColors();
|
|
const bounce = useRef(new Animated.Value(0)).current;
|
|
const pulse = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
const bounceLoop = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(bounce, {
|
|
toValue: 1,
|
|
duration: 600,
|
|
useNativeDriver: true,
|
|
easing: Easing.out(Easing.cubic),
|
|
}),
|
|
Animated.timing(bounce, {
|
|
toValue: 0,
|
|
duration: 600,
|
|
useNativeDriver: true,
|
|
easing: Easing.in(Easing.cubic),
|
|
}),
|
|
]),
|
|
);
|
|
const pulseLoop = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulse, { toValue: 1, duration: 900, useNativeDriver: true }),
|
|
Animated.timing(pulse, { toValue: 0, duration: 900, useNativeDriver: true }),
|
|
]),
|
|
);
|
|
bounceLoop.start();
|
|
pulseLoop.start();
|
|
return () => {
|
|
bounceLoop.stop();
|
|
pulseLoop.stop();
|
|
};
|
|
}, [bounce, pulse]);
|
|
|
|
const translateY = bounce.interpolate({ inputRange: [0, 1], outputRange: [0, -6] });
|
|
const arrowOpacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] });
|
|
const arrowScale = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.12] });
|
|
|
|
return (
|
|
<View style={{ alignItems: 'center', marginTop: 8 }}>
|
|
{/* Animated Up-Arrow zeigt auf den Screenshot (auf dessen unteren Button) */}
|
|
<Animated.View
|
|
style={{
|
|
opacity: arrowOpacity,
|
|
transform: [{ translateY }, { scale: arrowScale }],
|
|
}}
|
|
>
|
|
<Ionicons name="caret-up" size={28} color={colors.brandOrange} />
|
|
</Animated.View>
|
|
|
|
{/* Label-Pille */}
|
|
<Animated.View
|
|
style={{
|
|
marginTop: 4,
|
|
backgroundColor: colors.brandOrange,
|
|
borderRadius: 999,
|
|
paddingVertical: 8,
|
|
paddingHorizontal: 16,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
transform: [{ translateY }],
|
|
}}
|
|
>
|
|
<Text style={{ fontSize: 16 }}>👆</Text>
|
|
<Text
|
|
style={{
|
|
fontFamily: 'Nunito_700Bold',
|
|
fontSize: 14,
|
|
color: '#ffffff',
|
|
letterSpacing: 0.2,
|
|
}}
|
|
>
|
|
{buttonLabel}
|
|
</Text>
|
|
</Animated.View>
|
|
</View>
|
|
);
|
|
}
|