diff --git a/apps/rebreak-native/CHANGELOG.md b/apps/rebreak-native/CHANGELOG.md index 8863db1..348e959 100644 --- a/apps/rebreak-native/CHANGELOG.md +++ b/apps/rebreak-native/CHANGELOG.md @@ -14,19 +14,35 @@ Versioning: `version` follows SemVer, `versionCode` is monotonically increasing. ### Added - **Duo-style Onboarding (9 Slides)**: Vollständiger Onboarding-Flow — Welcome → Privacy → Nickname → DiGA-Choice → DiGA-Code → Plan → Payment → Protection → Done. Lyra-Bubble mit TTS-Audio-Button auf jedem Slide. Pre-Explainer-Screenshots vor iOS-Permission-Dialogen. Confetti-Animation + Top-5-FAQ-Accordion auf Done-Screen. -- **DiGA-Redeem-Endpoint + 10 Test-Codes**: Backend-Endpoint für DiGA-Code-Einlösung. 10 vordefinierte Test-Codes (REBREAK-TEST-001 bis REBREAK-TEST-010) für QA und Reviewer. +- **DiGA-Redeem-Endpoint + 100 Test-Codes**: Backend-Endpoint für DiGA-Code-Einlösung. 100 vordefinierte Test-Codes (REBREAK-TEST-001 bis REBREAK-TEST-100) für QA und Reviewer. +- **Lyra Voice TTS Auto-Play + Voice-Button in Bubble**: TTS startet automatisch beim Slide-Einblenden; Voice-Button in der Lyra-Bubble startet/stoppt Audio on-demand. DiGA-Accessibility-Feature. +- **Stripe Web-Checkout-Integration**: Pro 3,99 EUR/Monat, Legend 7,99 EUR/Monat, je 14-Tage-Trial. Checkout-Redirect via `expo-web-browser`, Webhook bestätigt Plan-Aktivierung serverseitig. - **Arabisch (Arabic) + RTL-Support**: Vollständige arabische Lokalisation mit automatischem RTL-Layout-Switching. - **NEFilter Robust Disable**: 2-step Apple-Pattern für zuverlässiges Deaktivieren des URL-Filters. `resetUrlFilter` als Recovery-Pfad bei code-5-Fehlern. `ProtectionOffSheet` ersetzt bisherigen Alert. -- **Family Controls always-on**: Kein "Bald"-Placeholder mehr — FamilyControls-Entitlement ist vollständig aktiv. +- **Family Controls always-on**: Kein "Bald"-Placeholder mehr — FamilyControls-Entitlement (Distribution-approved) ist vollständig aktiv. - **Stripe Tier-Rename + Checkout-Refactor**: Pläne heißen jetzt konsistent `pro` / `legend` (statt alter Naming-Varianten). Checkout-Endpoint neu strukturiert. -- **protectionDisabledAt (Backend)**: Server-seitige Timestamps verhindern Auto-Reaktivierung nach manuellem Deaktivieren. +- **protectionDisabledAt (Backend)**: Server-seitige Timestamps verhindern Auto-Reaktivierung nach manuellem Deaktivieren durch den User. +- **FaqAccordion-Komponente (shared)**: FAQ-Accordion extrahiert als `components/FaqAccordion` — geteilt zwischen DoneSlide und `help/faq`-Seite. + +### Changed +- **ProtectionSlide: Platform.OS-Dispatch**: iOS-Pfad aktiviert NEFilter + FamilyControls. Android-Pfad aktiviert VPN-Permission + a11y-TamperLock (VPN-Permission-Dialog → a11y-Pre-Explainer → a11y-Settings-Open → AppState-Auto-Detect-Return → tamperLock armed → finished). +- **Blocker Single-Banner-Logik**: `lockedIn` ist nur `true` wenn BEIDE Schutz-Ebenen aktiv sind (iOS: urlFilter && appDeletionLock; Android: VPN && a11y). Kein falsches "Locked In" mehr bei Teilschutz. ### Fixed +- **Android Keyboard-Covers-Input (Chat/DM)**: `react-native-keyboard-controller` ersetzt bisherigen KeyboardAvoidingView-Workaround — Input-Feld bleibt beim Tippen korrekt sichtbar. - **Protection Cooldown Auto-Disable Race**: Stale-State nach Cooldown-Ablauf korrigiert — kein falscher "aktiv"-Zustand mehr. -- **Blocker: lockedIn requires both layers**: Grüner "Locked In"-Banner erscheint nur noch, wenn beide Schutz-Ebenen (VPN + a11y) aktiv sind. - **Nickname Validation + Duplicate-Check**: Echtzeit-Prüfung auf bereits vergebene Nicknames im Onboarding. - **DiGA-Code Auto-Format**: Code-Eingabe formatiert automatisch (Großbuchstaben, Bindestriche). +### i18n +- Französisch (fr) — 3. Sprache (war bereits in v0.2.x als Beta eingeführt, jetzt vollständig). +- Arabisch (ar) + RTL — neu in v0.3.0. + +### Backend +- `protectionDisabledAt`-Timestamp auf User-Profil — verhindert Auto-Reactivation nach manuellem Disable. +- DiGA-Code-Redemption-Endpoint (`POST /api/diga/redeem`) — 100 Test-Codes, Plan-Hochstufung auf `legend`. +- Migration `20260517_add_lyra_post_i18n_key`: `i18n_key TEXT` auf `community_posts` (nullable, non-blocking). **Pending Deploy auf Hetzner** — Feature-Flag `USE_TEMPLATE_CATALOG=false` (default) haelt dieses Feature deaktiviert bis Migration deployed ist. + --- ## [0.2.1] — versionCode 9 — 2026-05-16 diff --git a/apps/rebreak-native/app/help/faq.tsx b/apps/rebreak-native/app/help/faq.tsx index 3f88ed4..6cb28f1 100644 --- a/apps/rebreak-native/app/help/faq.tsx +++ b/apps/rebreak-native/app/help/faq.tsx @@ -1,68 +1,9 @@ -import { useState } from 'react'; -import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; +import { ScrollView, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useColors } from '../../lib/theme'; import { AppHeader } from '../../components/AppHeader'; - -type FaqItem = { q: string; a: string }; - -function FaqRow({ q, a, colors }: FaqItem & { colors: ReturnType }) { - const [open, setOpen] = useState(false); - - return ( - - setOpen((v) => !v)} - activeOpacity={0.7} - style={{ - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingVertical: 14, - gap: 12, - }} - > - - {q} - - - - {open ? ( - - {a} - - ) : null} - - ); -} +import { FaqAccordion, type FaqItem } from '../../components/FaqAccordion'; export default function FaqScreen() { const { t } = useTranslation(); @@ -93,27 +34,7 @@ export default function FaqScreen() { }} showsVerticalScrollIndicator={false} > - - {items.map((item, i) => ( - - ))} - + ); diff --git a/apps/rebreak-native/components/FaqAccordion.tsx b/apps/rebreak-native/components/FaqAccordion.tsx new file mode 100644 index 0000000..85f2aac --- /dev/null +++ b/apps/rebreak-native/components/FaqAccordion.tsx @@ -0,0 +1,178 @@ +import { useState } from 'react'; +import { Text, TouchableOpacity, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useColors } from '../lib/theme'; + +/** + * Shared FAQ Accordion-Komponente. + * + * Nutzung: + * + * + * Varianten: + * - 'card' (default) — gruppiert in einer Card mit Trennlinien zwischen Zeilen, + * so wie auf der Settings → FAQ-Seite (iOS-Listen-Pattern). + * - 'pills' — jede Frage als eigene pill-Card, kompakt für inline-Embeds + * (z.B. End-of-Onboarding Top-5-Block). + * + * Eltern hat die Verantwortung für padding/marginHorizontal/ScrollView-Wrap. Die + * Komponente kümmert sich NUR um Rendering der Zeilen + Expand/Collapse-State. + */ + +export type FaqItem = { q: string; a: string }; + +type Variant = 'card' | 'pills'; + +export function FaqAccordion({ + items, + variant = 'card', +}: { + items: FaqItem[]; + variant?: Variant; +}) { + const colors = useColors(); + + if (variant === 'pills') { + return ( + + {items.map((item, i) => ( + + ))} + + ); + } + + return ( + + {items.map((item, i) => ( + + ))} + + ); +} + +function CardRow({ q, a, isLast }: { q: string; a: string; isLast: boolean }) { + const colors = useColors(); + const [open, setOpen] = useState(false); + return ( + + setOpen((v) => !v)} + activeOpacity={0.7} + style={{ + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 14, + gap: 12, + }} + > + + {q} + + + + {open ? ( + + {a} + + ) : null} + + ); +} + +function PillRow({ q, a }: { q: string; a: string }) { + const colors = useColors(); + const [open, setOpen] = useState(false); + return ( + + setOpen((v) => !v)} + activeOpacity={0.7} + style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }} + > + + {q} + + + + {open ? ( + + {a} + + ) : null} + + ); +} diff --git a/apps/rebreak-native/components/onboarding/ScreenshotPointer.tsx b/apps/rebreak-native/components/onboarding/ScreenshotPointer.tsx index 872a3c1..19aee0d 100644 --- a/apps/rebreak-native/components/onboarding/ScreenshotPointer.tsx +++ b/apps/rebreak-native/components/onboarding/ScreenshotPointer.tsx @@ -27,11 +27,25 @@ import { useColors } from '../../lib/theme'; */ export function ScreenshotPointer({ buttonLabel, + alignment = 'center', }: { /** Der Text auf dem korrekten Button im iOS-Dialog. Wird ins Label übernommen. */ buttonLabel: string; + /** + * Horizontale Position des Pointer-Pakets relativ zum Screenshot. + * 'center' = unter zentrierten Buttons (z.B. iOS NEFilter "Erlauben" unten). + * 'left' = unter links-positioniertem Button (z.B. iOS Family Controls + * "Fortfahren"/"Continuer" — Apple platziert die Zustimmung + * links-grau, Decline rechts-blau bei Screen-Time-Permission). + * 'right' = unter rechts-positioniertem Button (symmetrisch). + */ + alignment?: 'left' | 'center' | 'right'; }) { const colors = useColors(); + // Fixed Offset in dp — auf einem 390pt-iPhone shiftet das ~80pt vom Mittel- + // punkt weg, das matched die Button-Position innerhalb des ~80%-breiten + // System-Dialog-Modals gut genug ohne pixel-genaue Kalibrierung. + const offsetX = alignment === 'left' ? -80 : alignment === 'right' ? 80 : 0; const bounce = useRef(new Animated.Value(0)).current; const pulse = useRef(new Animated.Value(0)).current; @@ -72,11 +86,11 @@ export function ScreenshotPointer({ return ( - {/* Animated Up-Arrow zeigt auf den Screenshot (auf dessen unteren Button) */} + {/* Animated Up-Arrow zeigt auf den Screenshot (auf dessen relevanten Button) */} @@ -93,7 +107,7 @@ export function ScreenshotPointer({ flexDirection: 'row', alignItems: 'center', gap: 8, - transform: [{ translateY }], + transform: [{ translateX: offsetX }, { translateY }], }} > 👆 diff --git a/apps/rebreak-native/components/onboarding/slides/DoneSlide.tsx b/apps/rebreak-native/components/onboarding/slides/DoneSlide.tsx index cc3c1e7..94d0e28 100644 --- a/apps/rebreak-native/components/onboarding/slides/DoneSlide.tsx +++ b/apps/rebreak-native/components/onboarding/slides/DoneSlide.tsx @@ -1,13 +1,15 @@ -import { useEffect, useRef, useState } from 'react'; -import { Animated, Easing, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native'; +import { useEffect, useRef } from 'react'; +import { Animated, Easing, Text, useWindowDimensions, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { Ionicons } from '@expo/vector-icons'; import { useColors } from '../../../lib/theme'; import { OnboardingShell } from '../OnboardingShell'; import { LyraBubble } from '../LyraBubble'; import { CTABar } from '../CTABar'; +import { FaqAccordion, type FaqItem } from '../../FaqAccordion'; -const FAQ_KEYS = ['faq_q1', 'faq_q2', 'faq_q3', 'faq_q4', 'faq_q5'] as const; +// Top-5 (kuratiert für Onboarding-Ende) — alle 8 sind unter app/help/faq.tsx. +const ONBOARDING_FAQ_IDS = [1, 2, 4, 5, 8] as const; export function DoneSlide({ onEnter, @@ -23,6 +25,11 @@ export function DoneSlide({ const scale = useRef(new Animated.Value(0.6)).current; const opacity = useRef(new Animated.Value(0)).current; + const faqItems: FaqItem[] = ONBOARDING_FAQ_IDS.map((id) => ({ + q: t(`help.faq_q${id}`), + a: t(`help.faq_a${id}`), + })); + useEffect(() => { Animated.parallel([ Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 5, tension: 90 }), @@ -95,7 +102,7 @@ export function DoneSlide({ - {/* Inline Top-5-FAQ Accordion */} + {/* Inline Top-5-FAQ Accordion (pills-Variante) */} {t('onboarding.done.faq_section_title')} - - {FAQ_KEYS.map((qkey) => ( - - ))} - + ); @@ -195,65 +193,3 @@ function ConfettiOverlay() { ); } -// ─── FAQ Accordion-Row ─────────────────────────────────────────────────────── - -function FaqRow({ - question, - answer, - colors, -}: { - question: string; - answer: string; - colors: import('../../../lib/theme').ColorScheme; -}) { - const [expanded, setExpanded] = useState(false); - return ( - - setExpanded((v) => !v)} - activeOpacity={0.7} - style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }} - > - - {question} - - - - {expanded ? ( - - {answer} - - ) : null} - - ); -} diff --git a/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx b/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx index 7420dd3..3c392dc 100644 --- a/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx +++ b/apps/rebreak-native/components/onboarding/slides/ProtectionSlide.tsx @@ -154,6 +154,7 @@ function IosProtectionSlide({ ctaKey="onboarding.protection.cta_primary" buttonLabelKey="onboarding.protection.dialog_button_continue" markerHintKey="onboarding.protection.tap_marker_hint" + pointerAlignment="left" activating={activating} onActivate={activateAppLock} current={current} @@ -369,6 +370,7 @@ function PreExplainer({ ctaKey, buttonLabelKey, markerHintKey, + pointerAlignment = 'center', activating, onActivate, current, @@ -381,6 +383,7 @@ function PreExplainer({ ctaKey: string; buttonLabelKey: string; markerHintKey: string; + pointerAlignment?: 'left' | 'center' | 'right'; activating: boolean; onActivate: () => void; current: number; @@ -441,7 +444,7 @@ function PreExplainer({ /> - +