From 312c668ae9ffdd654d6bbca8931cb194789e8cce Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Wed, 20 May 2026 04:20:22 +0200 Subject: [PATCH] feat(onboarding): back-button between steps + language switcher on welcome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Back-Button: - OnboardingNavContext liefert der Shell einen optionalen goBack-Handler (kein prop-drilling durch 8 Slides). - OnboardingShell: chevron-back links neben der Progress-Bar wenn goBack gesetzt ist. - Controller: goToLinearPrevious() + BACK_ALLOWED-Liste. Back nur auf privacy/nickname/diga_choice/plan/payment — NICHT welcome (erste), done (final), diga_code (eigener onBack), protection (Backend-Step + Permission-Flow). Language-Switcher: - WelcomeSlide: 4 Sprach-Pills (DE/EN/FR/AR) oben rechts. User kommt während Onboarding nicht zu Settings — sonst kein Weg die Sprache zu wechseln. setLanguage persistiert + flippt RTL für AR. --- apps/rebreak-native/app/onboarding/index.tsx | 95 +++++++++++-------- .../onboarding/OnboardingNavContext.tsx | 30 ++++++ .../components/onboarding/OnboardingShell.tsx | 29 +++++- .../onboarding/slides/WelcomeSlide.tsx | 54 ++++++++++- 4 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 apps/rebreak-native/components/onboarding/OnboardingNavContext.tsx diff --git a/apps/rebreak-native/app/onboarding/index.tsx b/apps/rebreak-native/app/onboarding/index.tsx index 840e6f3..eed7e23 100644 --- a/apps/rebreak-native/app/onboarding/index.tsx +++ b/apps/rebreak-native/app/onboarding/index.tsx @@ -11,6 +11,7 @@ import { PlanSlide } from '../../components/onboarding/slides/PlanSlide'; import { PaymentSlide } from '../../components/onboarding/slides/PaymentSlide'; import { ProtectionSlide } from '../../components/onboarding/slides/ProtectionSlide'; import { DoneSlide } from '../../components/onboarding/slides/DoneSlide'; +import { OnboardingNavProvider } from '../../components/onboarding/OnboardingNavContext'; /** * Duo-Style Onboarding — single route, state-machine intern. @@ -97,6 +98,18 @@ export default function OnboardingScreen() { setSlide(LINEAR_ORDER[idx + 1]); } + function goToLinearPrevious() { + const idx = LINEAR_ORDER.indexOf(slide); + if (idx <= 0) return; + setSlide(LINEAR_ORDER[idx - 1]); + } + + // Back erlaubt nur auf reinen Info-/Auswahl-Slides. NICHT auf: + // welcome (erste), done (final), diga_code (hat eigenen onBack), + // protection (interne Phasen + persistierter Backend-Step + Permission-Flow). + const BACK_ALLOWED: Slide[] = ['privacy', 'nickname', 'diga_choice', 'plan', 'payment']; + const canGoBack = BACK_ALLOWED.includes(slide); + function exitToApp() { router.replace('/(app)'); } @@ -135,42 +148,50 @@ export default function OnboardingScreen() { // Slide-Dispatch ──────────────────────────────────────────────────────────── - switch (slide) { - case 'welcome': - return ; - case 'privacy': - return ; - case 'nickname': - return ; - case 'diga_choice': - return ( - - ); - case 'diga_code': - return ( - setSlide('diga_choice')} - current={current} - total={total} - /> - ); - case 'plan': - return ; - case 'payment': - return ( - - ); - case 'protection': - return ( - - ); - case 'done': - return ; + function renderSlide() { + switch (slide) { + case 'welcome': + return ; + case 'privacy': + return ; + case 'nickname': + return ; + case 'diga_choice': + return ( + + ); + case 'diga_code': + return ( + setSlide('diga_choice')} + current={current} + total={total} + /> + ); + case 'plan': + return ; + case 'payment': + return ( + + ); + case 'protection': + return ( + + ); + case 'done': + return ; + } } + + return ( + + {renderSlide()} + + ); } diff --git a/apps/rebreak-native/components/onboarding/OnboardingNavContext.tsx b/apps/rebreak-native/components/onboarding/OnboardingNavContext.tsx new file mode 100644 index 0000000..d846879 --- /dev/null +++ b/apps/rebreak-native/components/onboarding/OnboardingNavContext.tsx @@ -0,0 +1,30 @@ +import { createContext, useContext, type ReactNode } from 'react'; + +/** + * Stellt der OnboardingShell einen optionalen Back-Handler bereit, ohne + * `onBack` durch alle 8 Slide-Komponenten prop-drillen zu müssen. + * + * Controller (app/onboarding/index.tsx) entscheidet pro Slide ob ein Back + * möglich ist (welcome = erste Slide, done = final → kein Back) und liefert + * entsprechend `goBack` oder `null`. + */ +const OnboardingBackContext = createContext<(() => void) | null>(null); + +export function OnboardingNavProvider({ + goBack, + children, +}: { + goBack: (() => void) | null; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +/** Gibt den Back-Handler zurück, oder null wenn die aktuelle Slide kein Back erlaubt. */ +export function useOnboardingBack(): (() => void) | null { + return useContext(OnboardingBackContext); +} diff --git a/apps/rebreak-native/components/onboarding/OnboardingShell.tsx b/apps/rebreak-native/components/onboarding/OnboardingShell.tsx index f52fca4..30ecbdf 100644 --- a/apps/rebreak-native/components/onboarding/OnboardingShell.tsx +++ b/apps/rebreak-native/components/onboarding/OnboardingShell.tsx @@ -1,8 +1,10 @@ import { ReactNode } from 'react'; -import { ScrollView, View } from 'react-native'; +import { ScrollView, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Ionicons } from '@expo/vector-icons'; import { useColors } from '../../lib/theme'; import { SlideProgress } from './SlideProgress'; +import { useOnboardingBack } from './OnboardingNavContext'; /** * Layout-Wrapper für alle Onboarding-Slides. @@ -32,6 +34,7 @@ export function OnboardingShell({ }) { const colors = useColors(); const insets = useSafeAreaInsets(); + const goBack = useOnboardingBack(); return ( @@ -40,9 +43,31 @@ export function OnboardingShell({ paddingTop: insets.top + 12, paddingHorizontal: 20, paddingBottom: 8, + flexDirection: 'row', + alignItems: 'center', + gap: 12, }} > - + {goBack ? ( + + + + ) : null} + + + s.language); + const setLanguage = useLanguageStore((s) => s.setLanguage); + return ( + + {LANGS.map((l) => { + const active = language === l.code; + return ( + setLanguage(l.code)} + activeOpacity={0.7} + style={{ + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 8, + backgroundColor: active ? colors.brandOrange : colors.surfaceElevated, + }} + > + + {l.label} + + + ); + })} + + ); +} + /** * Slide 1: Lyra stellt sich vor + erklärt die Mission in einem Satz. * Hat den langen Empathie-Touch, weil's der erste Eindruck nach Signup ist. @@ -28,6 +76,10 @@ export function WelcomeSlide({ total={total} cta={} > + + + +