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",