chahinebrini 33aa3464b8 feat(onboarding): protection pointer redesign + i18n screenshots + lockedIn fix
## 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>
2026-05-17 19:58:56 +02:00

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>
);
}