diff --git a/apps/rebreak-native/assets/onboarding/ar/android-a11y-confirm-dialog-001.png b/apps/rebreak-native/assets/onboarding/ar/android-a11y-confirm-dialog-001.png
new file mode 100644
index 0000000..dff2fc9
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/ar/android-a11y-confirm-dialog-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/ar/android-a11y-overview-001.png b/apps/rebreak-native/assets/onboarding/ar/android-a11y-overview-001.png
new file mode 100644
index 0000000..d770366
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/ar/android-a11y-overview-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/ar/android-a11y-rebreak-row-001.png b/apps/rebreak-native/assets/onboarding/ar/android-a11y-rebreak-row-001.png
new file mode 100644
index 0000000..bf5818e
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/ar/android-a11y-rebreak-row-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/ar/android-a11y-toggle-on-001.png b/apps/rebreak-native/assets/onboarding/ar/android-a11y-toggle-on-001.png
new file mode 100644
index 0000000..e51ccc2
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/ar/android-a11y-toggle-on-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/ar/android-vpn-permission-001.png b/apps/rebreak-native/assets/onboarding/ar/android-vpn-permission-001.png
new file mode 100644
index 0000000..b98fea6
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/ar/android-vpn-permission-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/de/android-a11y-confirm-dialog-001.png b/apps/rebreak-native/assets/onboarding/de/android-a11y-confirm-dialog-001.png
new file mode 100644
index 0000000..117cf15
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/de/android-a11y-confirm-dialog-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/de/android-a11y-overview-001.png b/apps/rebreak-native/assets/onboarding/de/android-a11y-overview-001.png
new file mode 100644
index 0000000..4d1adc0
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/de/android-a11y-overview-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/de/android-a11y-rebreak-row-001.png b/apps/rebreak-native/assets/onboarding/de/android-a11y-rebreak-row-001.png
new file mode 100644
index 0000000..9ca7cfa
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/de/android-a11y-rebreak-row-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/de/android-a11y-toggle-on-001.png b/apps/rebreak-native/assets/onboarding/de/android-a11y-toggle-on-001.png
new file mode 100644
index 0000000..94e1f47
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/de/android-a11y-toggle-on-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/de/android-vpn-permission-001.png b/apps/rebreak-native/assets/onboarding/de/android-vpn-permission-001.png
new file mode 100644
index 0000000..864469c
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/de/android-vpn-permission-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/en/android-a11y-confirm-dialog-001.png b/apps/rebreak-native/assets/onboarding/en/android-a11y-confirm-dialog-001.png
new file mode 100644
index 0000000..10cc27f
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/en/android-a11y-confirm-dialog-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/en/android-a11y-overview-001.png b/apps/rebreak-native/assets/onboarding/en/android-a11y-overview-001.png
new file mode 100644
index 0000000..d0ed225
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/en/android-a11y-overview-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/en/android-a11y-rebreak-row-001.png b/apps/rebreak-native/assets/onboarding/en/android-a11y-rebreak-row-001.png
new file mode 100644
index 0000000..b1c2c63
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/en/android-a11y-rebreak-row-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/en/android-a11y-toggle-on-001.png b/apps/rebreak-native/assets/onboarding/en/android-a11y-toggle-on-001.png
new file mode 100644
index 0000000..f66eb28
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/en/android-a11y-toggle-on-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/en/android-vpn-permission-001.png b/apps/rebreak-native/assets/onboarding/en/android-vpn-permission-001.png
new file mode 100644
index 0000000..562e742
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/en/android-vpn-permission-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/fr/android-a11y-confirm-dialog-001.png b/apps/rebreak-native/assets/onboarding/fr/android-a11y-confirm-dialog-001.png
new file mode 100644
index 0000000..b3802e4
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/fr/android-a11y-confirm-dialog-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/fr/android-a11y-overview-001.png b/apps/rebreak-native/assets/onboarding/fr/android-a11y-overview-001.png
new file mode 100644
index 0000000..8b24268
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/fr/android-a11y-overview-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/fr/android-a11y-rebreak-row-001.png b/apps/rebreak-native/assets/onboarding/fr/android-a11y-rebreak-row-001.png
new file mode 100644
index 0000000..58b15bf
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/fr/android-a11y-rebreak-row-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/fr/android-a11y-toggle-on-001.png b/apps/rebreak-native/assets/onboarding/fr/android-a11y-toggle-on-001.png
new file mode 100644
index 0000000..ed68e66
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/fr/android-a11y-toggle-on-001.png differ
diff --git a/apps/rebreak-native/assets/onboarding/fr/android-vpn-permission-001.png b/apps/rebreak-native/assets/onboarding/fr/android-vpn-permission-001.png
new file mode 100644
index 0000000..2e1c600
Binary files /dev/null and b/apps/rebreak-native/assets/onboarding/fr/android-vpn-permission-001.png differ
diff --git a/apps/rebreak-native/components/PostCard.tsx b/apps/rebreak-native/components/PostCard.tsx
index 6031889..ae83ac8 100644
--- a/apps/rebreak-native/components/PostCard.tsx
+++ b/apps/rebreak-native/components/PostCard.tsx
@@ -75,7 +75,11 @@ function PostCardImpl({ post, onCommentPress }: Props) {
}, [heartScale]);
const displayAuthor = post.repostOf ? post.repostOf.author : post.author;
- const displayContent = post.repostOf ? post.repostOf.content : post.content;
+ const rawContent = post.repostOf ? post.repostOf.content : post.content;
+ // i18n-aware content: wenn i18nKey gesetzt → übersetzten Text nehmen,
+ // sonst rawContent (Legacy-Verhalten unverändert).
+ const i18nKey = post.repostOf ? undefined : post.i18nKey;
+ const displayContent = i18nKey ? t(`lyra_posts.${i18nKey}`) : rawContent;
const displayImage = post.repostOf ? post.repostOf.imageUrl : post.imageUrl;
// Image aspect-ratio: ermittelt aus onLoad event.source.{width,height}.
diff --git a/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx b/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx
index 0e17a61..7420dd3 100644
--- a/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx
+++ b/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx
@@ -1,10 +1,11 @@
-import { useState } from 'react';
-import { Alert, Image, Platform, Text, useWindowDimensions, View } from 'react-native';
+import { useEffect, useRef, useState } from 'react';
+import { Alert, AppState, 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 RebreakProtection from '../../../modules/rebreak-protection';
import { getPermissionScreenshot } from '../../../lib/onboardingAssets';
import { OnboardingShell } from '../OnboardingShell';
import { LyraBubble } from '../LyraBubble';
@@ -14,32 +15,34 @@ import { PermissionDeniedSheet } from '../../PermissionDeniedSheet';
import i18n from '../../../lib/i18n';
/**
- * Onboarding Step Protection — 2 Phasen, beide mit Pre-Explainer-Modal das
- * den iOS-Permission-Dialog vorzeigt + Pulse-Marker auf den "Erlauben"-Button.
+ * Onboarding-Schutz-Step.
*
- * ┌──────────────────────────────────────────────────────────────┐
- * │ Phase A: preexplain_url │
- * │ Lyra: "Gleich kommt iOS-Dialog. Tippe ERLAUBEN." │
- * │ Screenshot vom NEFilter-Dialog + roter Pulse auf "Erlauben" │
- * │ CTA "Aktivieren" → triggers protection.activateUrlFilter() │
- * │ │
- * │ Phase B: preexplain_lock │
- * │ Lyra: "Jetzt App-Schutz. Tippe FORTFAHREN." │
- * │ Screenshot vom Screen-Time-Dialog + roter Pulse │
- * │ CTA "Aktivieren" → triggers protection.activateFamilyControls │
- * │ │
- * │ Phase C: done → onDone() │
- * └──────────────────────────────────────────────────────────────┘
+ * Platform.OS-Dispatch:
+ * iOS → IosProtectionSlide (NEFilter + Family-Controls)
+ * Android → AndroidProtectionSlide (VpnService + Accessibility-Tamper-Lock)
*
- * Wenn URL-Filter fehlschlägt mit code 5 → PermissionDeniedSheet öffnet sich
- * (Retry-Pfad via resetUrlFilter()). Family-Controls hat keinen analogen
- * Recovery-Sheet — User muss in Settings → Bildschirmzeit den App-Zugriff
- * gewähren.
+ * Beide haben den gleichen Eltern-Vertrag (current/total/onDone) und nutzen
+ * den gleichen Pre-Explainer + Lyra-Bubble + CTA-Pattern — die Innereien
+ * unterscheiden sich nur in (a) welche Permission-Dialoge geöffnet werden
+ * und (b) welche Screenshots gezeigt werden.
*/
-type Phase = 'preexplain_url' | 'preexplain_lock' | 'done';
+export function ProtectionSlide(props: {
+ onDone: () => void;
+ current: number;
+ total: number;
+}) {
+ if (Platform.OS === 'android') {
+ return ;
+ }
+ return ;
+}
-export function ProtectionSlide({
+// ─── iOS ────────────────────────────────────────────────────────────────────
+
+type IosPhase = 'preexplain_url' | 'preexplain_lock' | 'done';
+
+function IosProtectionSlide({
onDone,
current,
total,
@@ -49,8 +52,7 @@ export function ProtectionSlide({
total: number;
}) {
const { t } = useTranslation();
- const colors = useColors();
- const [phase, setPhase] = useState('preexplain_url');
+ const [phase, setPhase] = useState('preexplain_url');
const [activating, setActivating] = useState(false);
const [permissionDeniedOpen, setPermissionDeniedOpen] = useState(false);
@@ -61,7 +63,6 @@ export function ProtectionSlide({
const res = await protection.activateUrlFilter();
if (!res.enabled) {
const isCodeFive =
- Platform.OS === 'ios' &&
typeof res.error === 'string' &&
/NEFilterErrorDomain:\s*5/i.test(res.error);
if (isCodeFive) {
@@ -74,7 +75,6 @@ export function ProtectionSlide({
);
return;
}
- // Filter live → weiter zur Phase B (App-Lock)
setPhase('preexplain_lock');
} finally {
setActivating(false);
@@ -87,8 +87,6 @@ export function ProtectionSlide({
try {
const res = await protection.activateFamilyControls();
if (!res.enabled) {
- // Family Controls fehlgeschlagen → User Info aber Tour-Done (URL-Filter
- // läuft schon, das ist der Hauptschutz; App-Lock ist optional)
Alert.alert(
t('onboarding.protection.applock_failed_title'),
res.error ?? t('onboarding.protection.applock_failed_msg'),
@@ -119,41 +117,247 @@ export function ProtectionSlide({
onDone();
}
- return phase === 'preexplain_url' ? (
-
- setPermissionDeniedOpen(false)}
- onRetry={async () => {
- const res = await protection.resetUrlFilter();
- if (res.enabled) setPhase('preexplain_lock');
- return res;
- }}
+ if (phase === 'preexplain_url') {
+ return (
+
+ setPermissionDeniedOpen(false)}
+ onRetry={async () => {
+ const res = await protection.resetUrlFilter();
+ if (res.enabled) setPhase('preexplain_lock');
+ return res;
+ }}
+ />
+
+ );
+ }
+ if (phase === 'preexplain_lock') {
+ return (
+
-
- ) : phase === 'preexplain_lock' ? (
- void;
+ current: number;
+ total: number;
+}) {
+ const { t } = useTranslation();
+ const [phase, setPhase] = useState('preexplain_vpn');
+ const [activating, setActivating] = useState(false);
+ // True wenn wir auf Settings-Rückkehr warten. AppState-Listener pollt dann
+ // a11y-State + advanced automatisch wenn ReBreak-Schalter live ist.
+ const awaitingReturnRef = useRef(false);
+ const appStateRef = useRef(AppState.currentState);
+
+ async function finishProtectionStep() {
+ await apiFetch('/api/profile/me/onboarding-step', {
+ method: 'PATCH',
+ body: { step: 'done' },
+ }).catch(() => {});
+ invalidateMe();
+ setPhase('done');
+ onDone();
+ }
+
+ async function activateVpn() {
+ if (activating) return;
+ setActivating(true);
+ try {
+ const res = await protection.activateUrlFilter();
+ if (!res.enabled) {
+ Alert.alert(
+ t('onboarding.protection.error_title'),
+ res.error ?? t('onboarding.protection.error_unknown'),
+ );
+ return;
+ }
+ setPhase('preexplain_a11y');
+ } finally {
+ setActivating(false);
+ }
+ }
+
+ async function activateA11y() {
+ if (activating) return;
+ setActivating(true);
+ try {
+ const res = await protection.activateFamilyControls();
+ if (res.enabled) {
+ // Selten: User hatte a11y schon manuell aktiviert → Lock direkt armed.
+ finishProtectionStep();
+ return;
+ }
+ if (res.error === 'accessibility_pending') {
+ // Native hat Settings geöffnet; warte auf Rückkehr + poll.
+ awaitingReturnRef.current = true;
+ setPhase('a11y_pending');
+ return;
+ }
+ Alert.alert(
+ t('onboarding.protection.error_title'),
+ res.error ?? t('onboarding.protection.error_unknown'),
+ );
+ } finally {
+ setActivating(false);
+ }
+ }
+
+ // Auto-Check beim Foreground-Return: wenn a11y jetzt aktiv → Lock armen + done.
+ useEffect(() => {
+ const sub = AppState.addEventListener('change', async (next) => {
+ const prev = appStateRef.current;
+ appStateRef.current = next;
+ if (!awaitingReturnRef.current) return;
+ if (prev.match(/inactive|background/) && next === 'active') {
+ try {
+ const a11y = await RebreakProtection.isAccessibilityEnabled();
+ if (a11y.enabled) {
+ // ReBreak-Service ist live → Tamper-Lock armen + finish.
+ const res = await protection.activateFamilyControls();
+ if (res.enabled) {
+ awaitingReturnRef.current = false;
+ finishProtectionStep();
+ }
+ }
+ } catch {
+ // Ignorieren — User kann manuell auf "Ich habe ReBreak aktiviert" tippen.
+ }
+ }
+ });
+ return () => sub.remove();
+ }, []);
+
+ if (phase === 'preexplain_vpn') {
+ return (
+
+ );
+ }
+ if (phase === 'preexplain_a11y') {
+ return (
+
+ );
+ }
+ if (phase === 'a11y_pending') {
+ return (
+
+ );
+ }
+ return null;
+}
+
+function A11yPendingView({
+ current,
+ total,
+ activating,
+ onRetry,
+}: {
+ current: number;
+ total: number;
+ activating: boolean;
+ onRetry: () => void;
+}) {
+ const { t } = useTranslation();
+ const colors = useColors();
+ return (
+
- ) : null;
+ cta={
+
+ }
+ >
+
+
+ {t('onboarding.protection.android_a11y_pending_title')}
+
+
+ );
}
// ─── PreExplainer (shared) ───────────────────────────────────────────────────
@@ -163,16 +367,20 @@ function PreExplainer({
lyraBodyKey,
titleKey,
ctaKey,
+ buttonLabelKey,
+ markerHintKey,
activating,
onActivate,
current,
total,
children,
}: {
- dialog: 'url_filter' | 'screen_time';
+ dialog: 'url_filter' | 'screen_time' | 'android_vpn' | 'android_a11y';
lyraBodyKey: string;
titleKey: string;
ctaKey: string;
+ buttonLabelKey: string;
+ markerHintKey: string;
activating: boolean;
onActivate: () => void;
current: number;
@@ -184,10 +392,6 @@ function PreExplainer({
const { height: screenH } = useWindowDimensions();
const lang = i18n.language || 'de';
const screenshot = getPermissionScreenshot(dialog, lang);
- 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
@@ -222,8 +426,6 @@ function PreExplainer({
{t(titleKey)}
- {/* Screenshot — Modal hat eigene runde Ecken im Bild, kein extra Container-
- Radius (sonst Double-Round-Look). Nur padding für Atmungsraum. */}
- {/* Animierter Pointer UNTER dem Screenshot — Dimensions-agnostic. */}
- {t('onboarding.protection.tap_marker_hint')}
+ {t(markerHintKey)}
{children}
diff --git a/apps/rebreak-native/lib/onboardingAssets.ts b/apps/rebreak-native/lib/onboardingAssets.ts
index 25a97d3..7000a27 100644
--- a/apps/rebreak-native/lib/onboardingAssets.ts
+++ b/apps/rebreak-native/lib/onboardingAssets.ts
@@ -1,15 +1,23 @@
/**
- * iOS Permission-Dialog-Screenshots für den Onboarding-Pre-Explainer.
+ * Permission-Dialog-Screenshots für den Onboarding-Pre-Explainer.
*
* Pro Sprache liegen die Screenshots in `assets/onboarding//`. Falls
* eine Sprache nicht (oder noch nicht) verfügbar ist, fällt der Resolver auf
* `de` zurück.
*
+ * Dialog-Typen:
+ * iOS:
+ * - `url_filter` → NEFilter-System-Dialog ("Erlauben"-Button)
+ * - `screen_time` → Family-Controls-/Screen-Time-Dialog ("Fortfahren")
+ * Android:
+ * - `android_vpn` → VpnService-System-Dialog ("OK"-Button)
+ * - `android_a11y` → Bedienungshilfen-Settings mit ReBreak-Eintrag
+ *
* Diese Maps explizit mit `require(...)` deklarieren — RN/Metro kann keine
* dynamischen Pfade auflösen.
*/
-type Dialog = 'url_filter' | 'screen_time';
+type Dialog = 'url_filter' | 'screen_time' | 'android_vpn' | 'android_a11y';
type Lang = 'de' | 'en' | 'fr' | 'ar';
/* eslint-disable @typescript-eslint/no-require-imports */
@@ -22,6 +30,18 @@ const SCREEN_TIME_DE = require('../assets/onboarding/de/screen_time_permission.j
const SCREEN_TIME_EN = require('../assets/onboarding/en/screen_time_permission.jpeg');
const SCREEN_TIME_FR = require('../assets/onboarding/fr/screen_time_permission.jpeg');
const SCREEN_TIME_AR = require('../assets/onboarding/ar/screen_time_permission.jpeg');
+
+// Android — VpnService-Permission-Dialog ("Verbindungsanforderung")
+const ANDROID_VPN_DE = require('../assets/onboarding/de/android-vpn-permission-001.png');
+const ANDROID_VPN_EN = require('../assets/onboarding/en/android-vpn-permission-001.png');
+const ANDROID_VPN_FR = require('../assets/onboarding/fr/android-vpn-permission-001.png');
+const ANDROID_VPN_AR = require('../assets/onboarding/ar/android-vpn-permission-001.png');
+
+// Android — Accessibility-Settings, ReBreak-Row sichtbar
+const ANDROID_A11Y_DE = require('../assets/onboarding/de/android-a11y-rebreak-row-001.png');
+const ANDROID_A11Y_EN = require('../assets/onboarding/en/android-a11y-rebreak-row-001.png');
+const ANDROID_A11Y_FR = require('../assets/onboarding/fr/android-a11y-rebreak-row-001.png');
+const ANDROID_A11Y_AR = require('../assets/onboarding/ar/android-a11y-rebreak-row-001.png');
/* eslint-enable @typescript-eslint/no-require-imports */
const SCREENSHOTS: Record