diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx index 0c64a94..0d78dd4 100644 --- a/apps/rebreak-native/app/(app)/blocker.tsx +++ b/apps/rebreak-native/app/(app)/blocker.tsx @@ -74,7 +74,11 @@ export default function BlockerScreen() { const urlFilterActive = state?.layers.urlFilter === true; const familyControlsActive = state?.layers.familyControls === true; const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true; - const lockedIn = appDeletionLockActive; + // "lockedIn" = beide Layer aktiv: URL-Filter (echter Schutz) UND App-Lock + // (Hardening). Family-Controls ALLEINE = kein Schutz, nur denyAppRemoval — + // ohne URL-Filter sieht der User trotzdem Glücksspielseiten. Daher BEIDE + // müssen an sein damit der "Schutz aktiv"-Banner gezeigt wird. + const lockedIn = urlFilterActive && appDeletionLockActive; const urlFilterActiveRef = useRef(urlFilterActive); useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]); @@ -270,6 +274,7 @@ export default function BlockerScreen() { active={appDeletionLockActive} onActivate={handleActivateFamilyControls} warning={t('blocker.layers_app_lock_warning')} + lockedHint={t('blocker.layers_app_lock_locked_hint')} /> )} diff --git a/apps/rebreak-native/assets/onboarding/ar/screen_time_permission.jpeg b/apps/rebreak-native/assets/onboarding/ar/screen_time_permission.jpeg new file mode 100644 index 0000000..1e2f071 Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/ar/screen_time_permission.jpeg differ diff --git a/apps/rebreak-native/assets/onboarding/ar/url_filter_permission.jpeg b/apps/rebreak-native/assets/onboarding/ar/url_filter_permission.jpeg new file mode 100644 index 0000000..0230d47 Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/ar/url_filter_permission.jpeg differ diff --git a/apps/rebreak-native/assets/onboarding/en/screen_time_permission.jpeg b/apps/rebreak-native/assets/onboarding/en/screen_time_permission.jpeg new file mode 100644 index 0000000..7e61b4e Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/en/screen_time_permission.jpeg differ diff --git a/apps/rebreak-native/assets/onboarding/en/url_filter_permission.jpeg b/apps/rebreak-native/assets/onboarding/en/url_filter_permission.jpeg new file mode 100644 index 0000000..a946859 Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/en/url_filter_permission.jpeg differ diff --git a/apps/rebreak-native/assets/onboarding/fr/screen_time_permission.jpeg b/apps/rebreak-native/assets/onboarding/fr/screen_time_permission.jpeg new file mode 100644 index 0000000..98d2c0c Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/fr/screen_time_permission.jpeg differ diff --git a/apps/rebreak-native/assets/onboarding/fr/url_filter_permission.jpeg b/apps/rebreak-native/assets/onboarding/fr/url_filter_permission.jpeg new file mode 100644 index 0000000..b4ed5e9 Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/fr/url_filter_permission.jpeg differ diff --git a/apps/rebreak-native/components/blocker/LayerSwitchCard.tsx b/apps/rebreak-native/components/blocker/LayerSwitchCard.tsx index 124820f..f85618b 100644 --- a/apps/rebreak-native/components/blocker/LayerSwitchCard.tsx +++ b/apps/rebreak-native/components/blocker/LayerSwitchCard.tsx @@ -11,11 +11,16 @@ type Props = { /** 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 für commit-heavy Layer. */ + /** 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 }: Props) { +export function LayerSwitchCard({ icon, title, subtitle, active, onActivate, warning, lockedHint }: Props) { const [busy, setBusy] = useState(false); async function handleSwitch(v: boolean) { @@ -106,6 +111,33 @@ export function LayerSwitchCard({ icon, title, subtitle, active, onActivate, war )} + + {lockedHint && active && ( + + + + {lockedHint} + + + )} ); } diff --git a/apps/rebreak-native/components/onboarding/OnboardingShell.tsx b/apps/rebreak-native/components/onboarding/OnboardingShell.tsx index a6f864a..f52fca4 100644 --- a/apps/rebreak-native/components/onboarding/OnboardingShell.tsx +++ b/apps/rebreak-native/components/onboarding/OnboardingShell.tsx @@ -48,9 +48,9 @@ export function OnboardingShell({ - * - * - * + * Jetzt: render-unter dem Screenshot. Layout-agnostic. Animation lenkt + * Aufmerksamkeit auf die richtige Region ohne pixel-genaue Position. * - * `xPercent=50, yPercent=87` ist die typische Position für "Erlauben"/ - * "Fortfahren" am Bottom des iOS-Permission-Dialogs. + * ┌──────────────────────────┐ + * │ │ + * │ [Nicht erlauben] │ + * │ [Erlauben] │ + * └──────────────────────────┘ + * ▲ + * ┌─────────────┐ + * │ ▲ Tippe │ ← Animated bouncing pill mit Pfeil + Label + * │ "Erlauben" │ + * └─────────────┘ */ export function ScreenshotPointer({ - xPercent, - yPercent, - color = '#dc2626', + buttonLabel, }: { - /** Horizontale Position als 0..100 vom Container-Width */ - xPercent: number; - /** Vertikale Position als 0..100 vom Container-Height */ - yPercent: number; - /** Pulse-Color. Default rot — fällt auf, signalisiert "wichtig". */ - color?: string; + /** 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 loop = Animated.loop( + const bounceLoop = Animated.loop( Animated.sequence([ - Animated.timing(pulse, { + Animated.timing(bounce, { toValue: 1, - duration: 1100, + duration: 600, useNativeDriver: true, easing: Easing.out(Easing.cubic), }), - Animated.timing(pulse, { + Animated.timing(bounce, { toValue: 0, - duration: 1100, + duration: 600, useNativeDriver: true, easing: Easing.in(Easing.cubic), }), ]), ); - loop.start(); - return () => loop.stop(); - }, [pulse]); + 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 scale = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.45] }); - const opacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.95, 0.2] }); + 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 ( - - {/* Outer pulse-ring */} + + {/* Animated Up-Arrow zeigt auf den Screenshot (auf dessen unteren Button) */} - {/* Inner solid ring */} - + + + + {/* Label-Pille */} + + > + 👆 + + {buttonLabel} + + ); } diff --git a/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx b/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx index 08da415..c06c3e3 100644 --- a/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx +++ b/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx @@ -1,11 +1,11 @@ import { useState } from 'react'; -import { Alert, Image, Platform, Text, View } from 'react-native'; +import { Alert, Image, Platform, Text, useWindowDimensions, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useColors } from '../../../lib/theme'; import { apiFetch } from '../../../lib/api'; import { invalidateMe } from '../../../hooks/useMe'; import { protection } from '../../../lib/protection'; -import { getPermissionScreenshot, getPointerPosition } from '../../../lib/onboardingAssets'; +import { getPermissionScreenshot } from '../../../lib/onboardingAssets'; import { OnboardingShell } from '../OnboardingShell'; import { LyraBubble } from '../LyraBubble'; import { CTABar } from '../CTABar'; @@ -181,9 +181,18 @@ function PreExplainer({ }) { const { t } = useTranslation(); const colors = useColors(); + const { height: screenH } = useWindowDimensions(); const lang = i18n.language || 'de'; const screenshot = getPermissionScreenshot(dialog, lang); - const pointer = getPointerPosition(dialog); + const buttonLabelKey = + dialog === 'url_filter' + ? 'onboarding.protection.dialog_button_allow' + : 'onboarding.protection.dialog_button_continue'; + + // Dynamische Screenshot-Höhe: Auf kleinen Phones (SE/mini ~667-844 pt) + // capped damit alles + CTA-Bar ohne Scroll passt. Auf großen Phones/iPad + // skaliert es mit. Min 200, Max 320. + const screenshotHeight = Math.min(320, Math.max(200, screenH * 0.32)); return ( - {/* Screenshot mit Pulse-Pointer auf den korrekten Button */} + {/* Screenshot — sauber ohne Overlay. Dynamisch dimensioniert. */} - + {/* Animierter Pointer UNTER dem Screenshot — Dimensions-agnostic. */} + + >> = { url_filter: { de: URL_FILTER_DE, + en: URL_FILTER_EN, + fr: URL_FILTER_FR, + ar: URL_FILTER_AR, }, screen_time: { de: SCREEN_TIME_DE, + en: SCREEN_TIME_EN, + fr: SCREEN_TIME_FR, + ar: SCREEN_TIME_AR, }, }; @@ -40,21 +51,7 @@ export function getPermissionScreenshot(dialog: Dialog, lang: string): number { return map[normalized] ?? map.de!; } -/** - * Wo der Pointer-Marker auf dem Screenshot positioniert werden soll. - * Werte sind Prozent (0..100) relativ zum Container. - * - * Beide iOS-Dialoge haben den korrekten Button am Bottom (~85-88% Y, 50% X). - * Falls Apple das Layout pro Locale anders rendert — pro-Lang-Overrides - * hier ergänzen. - */ -export function getPointerPosition(dialog: Dialog): { xPercent: number; yPercent: number } { - switch (dialog) { - case 'url_filter': - // Button "Erlauben" am unteren Rand des Modals - return { xPercent: 50, yPercent: 86 }; - case 'screen_time': - // Button "Fortfahren" am unteren Rand - return { xPercent: 50, yPercent: 86 }; - } -} +// (Deprecated) getPointerPosition entfernt — der Pointer wird jetzt extern +// UNTER dem Screenshot gerendert (ScreenshotPointer-Komponente), nicht mehr +// per-percent overlayed. Damit entfällt die Notwendigkeit pixel-genaue +// Positionen pro Locale + Dialog zu pflegen — siehe ScreenshotPointer.tsx. diff --git a/apps/rebreak-native/locales/ar.json b/apps/rebreak-native/locales/ar.json index 77206dd..9025df4 100644 --- a/apps/rebreak-native/locales/ar.json +++ b/apps/rebreak-native/locales/ar.json @@ -303,6 +303,7 @@ "layers_app_lock_subtitle_active": "مقفل — الإيقاف فقط عبر فترة التهدئة", "layers_app_lock_subtitle_inactive": "يمنع إيقاف rebreak أو الفلتر في لحظة الاندفاع", "layers_app_lock_warning": "بمجرد التفعيل لا يمكنك إيقاف الحماية إلا عبر تهدئة 24 ساعة. هذا مقصود.", + "layers_app_lock_locked_hint": "مقفل بواسطة النظام. الإيقاف فقط عبر إعدادات iOS → مدة استخدام الجهاز → الإدارة بواسطة ReBreak.", "layers_a11y_subtitle_active": "إمكانية الوصول نشطة — حماية التطبيق مفعّلة", "layers_a11y_subtitle_inactive": "إمكانية الوصول غير مفعّلة — قم بالإعداد الآن", "kpi_global_label": "النطاقات المحجوبة عالمياً", @@ -443,7 +444,9 @@ "cta_primary": "فعّل الحماية", "url_title": "الخطوة 1 من 2 — فلتر المحتوى", "lock_title": "الخطوة 2 من 2 — قفل التطبيق", - "tap_marker_hint": "العلامة الحمراء تشير إلى الزر الصحيح. Apple يضع الأزرق الكبير في الأعلى («عدم السماح») — لا تقع في الفخ.", + "tap_marker_hint": "Apple يضع الزر الأزرق الكبير في الأعلى («عدم السماح») — اضغط الزر السفلي، وليس العلوي.", + "dialog_button_allow": "اضغط «السماح»", + "dialog_button_continue": "اضغط «متابعة»", "applock_failed_title": "فشل قفل التطبيق", "applock_failed_msg": "يمكنك المحاولة مرة أخرى أو تخطي هذه الخطوة — فلتر URL يعمل بالفعل.", "applock_skip": "تخطّي", diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index bafdcd5..78dcd2f 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -303,6 +303,7 @@ "layers_app_lock_subtitle_active": "Verriegelt — Abschalten nur über die Abkühlphase", "layers_app_lock_subtitle_inactive": "Verhindert, dass du ReBreak oder den Filter im Impuls abschaltest", "layers_app_lock_warning": "Sobald aktiv kannst du den Schutz nur über einen 24-Stunden-Cooldown abschalten. Das ist gewollt.", + "layers_app_lock_locked_hint": "System-gesperrt. Deaktivierung nur in iOS-Einstellungen → Bildschirmzeit → Verwaltung durch ReBreak.", "layers_a11y_subtitle_active": "Eingabehilfe aktiv — App-Schutz armiert", "layers_a11y_subtitle_inactive": "Eingabehilfe nicht aktiviert — jetzt einrichten", "kpi_global_label": "Geblockte Domains weltweit", @@ -443,7 +444,9 @@ "cta_primary": "Schutz aktivieren", "url_title": "Schritt 1 von 2 — Inhaltsfilter", "lock_title": "Schritt 2 von 2 — App-Schutz", - "tap_marker_hint": "Der rote Marker zeigt den richtigen Button. Apple platziert den großen blauen oben (\"Nicht erlauben\") — bitte nicht reinfallen.", + "tap_marker_hint": "Apple platziert den großen blauen Button oben (\"Nicht erlauben\") — bitte den UNTEREN Button tippen, nicht den oberen.", + "dialog_button_allow": "Tippe \"Erlauben\"", + "dialog_button_continue": "Tippe \"Fortfahren\"", "applock_failed_title": "App-Schutz fehlgeschlagen", "applock_failed_msg": "Du kannst es nochmal versuchen oder den Schritt überspringen — der URL-Filter läuft schon.", "applock_skip": "Überspringen", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index 71ff9b3..5bc59bb 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -303,6 +303,7 @@ "layers_app_lock_subtitle_active": "Locked — disable only via the cooldown", "layers_app_lock_subtitle_inactive": "Stops you from switching off ReBreak or the filter on impulse", "layers_app_lock_warning": "Once active, you can only disable protection through a 24-hour cooldown. That's by design.", + "layers_app_lock_locked_hint": "System-locked. Only disable via iOS Settings → Screen Time → Management by ReBreak.", "layers_a11y_subtitle_active": "Accessibility active — app protection armed", "layers_a11y_subtitle_inactive": "Accessibility not enabled — set it up now", "kpi_global_label": "Domains blocked worldwide", @@ -443,7 +444,9 @@ "cta_primary": "Activate protection", "url_title": "Step 1 of 2 — Content filter", "lock_title": "Step 2 of 2 — App lock", - "tap_marker_hint": "The red marker shows the correct button. Apple puts the big blue one on top (\"Don't Allow\") — please don't fall for it.", + "tap_marker_hint": "Apple puts the big blue button on top (\"Don't Allow\") — please tap the BOTTOM button, not the top one.", + "dialog_button_allow": "Tap \"Allow\"", + "dialog_button_continue": "Tap \"Continue\"", "applock_failed_title": "App lock failed", "applock_failed_msg": "You can try again or skip this step — the URL filter is already running.", "applock_skip": "Skip", diff --git a/apps/rebreak-native/locales/fr.json b/apps/rebreak-native/locales/fr.json index 6ab91ec..e71e0cd 100644 --- a/apps/rebreak-native/locales/fr.json +++ b/apps/rebreak-native/locales/fr.json @@ -303,6 +303,7 @@ "layers_app_lock_subtitle_active": "Verrouillé — désactivation uniquement via la pause de sécurité", "layers_app_lock_subtitle_inactive": "Vous empêche de désactiver ReBreak ou le filtre sous l'impulsion", "layers_app_lock_warning": "Une fois actif, vous ne pouvez désactiver la protection que via une pause de sécurité de 24 heures. C'est voulu.", + "layers_app_lock_locked_hint": "Verrouillé par le système. Désactivation uniquement via Réglages iOS → Temps d'écran → Gestion par ReBreak.", "kpi_global_label": "Domaines bloqués dans le monde", "kpi_global_subtitle": "Entrées actives dans la liste de blocage globale", "delta_week": "cette semaine", @@ -441,7 +442,9 @@ "cta_primary": "Activer la protection", "url_title": "Étape 1 sur 2 — Filtre de contenu", "lock_title": "Étape 2 sur 2 — Verrou d'app", - "tap_marker_hint": "Le marqueur rouge indique le bon bouton. Apple place le grand bleu en haut (« Refuser ») — n'y tombe pas.", + "tap_marker_hint": "Apple place le grand bouton bleu en haut (« Refuser ») — touche le bouton du BAS, pas celui du haut.", + "dialog_button_allow": "Touche « Autoriser »", + "dialog_button_continue": "Touche « Continuer »", "applock_failed_title": "Échec du verrou d'app", "applock_failed_msg": "Tu peux réessayer ou ignorer cette étape — le filtre URL est déjà actif.", "applock_skip": "Ignorer",