From e9d34dbe78f6d12db6165883f5d6583172488364 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 11 May 2026 14:13:47 +0200 Subject: [PATCH] feat(settings): subscription section + __DEV__ plan-override toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - settings.tsx: real "Abo" row showing current plan (Free/Pro/Legend badge), taps open a sheet explaining subscriptions are managed on rebreak.org (Linking.openURL → /account; TODO: gate for iOS App-Store submission per Apple 3.1.1 — no in-app purchase flow) - debug.tsx: __DEV__-only plan-override toggle (free/pro/legend) via PATCH /api/admin/users/:id + invalidateMe(); shows admin-only hint on 403 - locales: settings.subscription_* keys (de + en) Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/rebreak-native/app/debug.tsx | 138 ++++++++++++++++++++++++++- apps/rebreak-native/app/settings.tsx | 128 ++++++++++++++++++++++++- apps/rebreak-native/locales/de.json | 6 ++ apps/rebreak-native/locales/en.json | 6 ++ 4 files changed, 275 insertions(+), 3 deletions(-) diff --git a/apps/rebreak-native/app/debug.tsx b/apps/rebreak-native/app/debug.tsx index bd86267..fb3a2fc 100644 --- a/apps/rebreak-native/app/debug.tsx +++ b/apps/rebreak-native/app/debug.tsx @@ -1,13 +1,16 @@ -import { useEffect } from 'react'; -import { View, Text, ScrollView, Pressable } from 'react-native'; +import { useEffect, useState } from 'react'; +import { View, Text, ScrollView, Pressable, Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useColors } from '../lib/theme'; +import { useMe, invalidateMe, type Plan } from '../hooks/useMe'; +import { apiFetch } from '../lib/api'; export default function DebugScreen() { const router = useRouter(); const colors = useColors(); + const { me } = useMe(); useEffect(() => { if (!__DEV__) { @@ -91,6 +94,14 @@ export default function DebugScreen() { + {me ? ( + + ) : null} + = { + free: '#737373', + pro: '#007AFF', + legend: '#f59e0b', +}; + +function PlanOverrideToggle({ + colors, + userId, + currentPlan, +}: { + colors: import('../lib/theme').ColorScheme; + userId: string; + currentPlan: Plan; +}) { + const [loading, setLoading] = useState(false); + + async function switchPlan(plan: Plan) { + if (plan === currentPlan) return; + setLoading(true); + try { + // PATCH /api/admin/users/:id requires admin privileges. + // If the dev-user is not admin, this returns 403 — see alert below. + await apiFetch(`/api/admin/users/${userId}`, { + method: 'PATCH', + body: JSON.stringify({ plan }), + }); + invalidateMe(); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + if (msg.includes('403')) { + Alert.alert( + 'Kein Admin-Zugriff', + 'PATCH /api/admin/users/:id setzt Admin-Rechte voraus. Plan manuell im Admin-Panel flippen.', + ); + } else { + Alert.alert('Fehler', msg); + } + } finally { + setLoading(false); + } + } + + return ( + + + + + + + + Plan-Override (DEV) + + + PATCH /api/admin/users/:id — braucht Admin-Rechte + + + + + + {PLANS.map((plan) => { + const isActive = plan === currentPlan; + const accent = PLAN_COLOR[plan]; + return ( + switchPlan(plan)} + disabled={loading || isActive} + style={({ pressed }) => ({ + flex: 1, + paddingVertical: 10, + borderRadius: 10, + alignItems: 'center', + backgroundColor: isActive ? accent : colors.surfaceElevated, + opacity: loading ? 0.5 : pressed ? 0.7 : 1, + })} + > + + {plan} + + + ); + })} + + + ); +} + function DebugStub({ title, subtitle, diff --git a/apps/rebreak-native/app/settings.tsx b/apps/rebreak-native/app/settings.tsx index 10b9edc..8d84f60 100644 --- a/apps/rebreak-native/app/settings.tsx +++ b/apps/rebreak-native/app/settings.tsx @@ -1,5 +1,6 @@ import { Alert, + Linking, Platform, Pressable, ScrollView, @@ -21,6 +22,114 @@ import { useLanguageStore, type AppLanguage } from '../stores/language'; import { useUserPlan } from '../hooks/useUserPlan'; import { AppHeader } from '../components/AppHeader'; +// ─── Subscription Sheet ──────────────────────────────────────────────────── + +type SubscriptionSheetProps = { + plan: 'free' | 'pro' | 'legend'; + colors: import('../lib/theme').ColorScheme; + t: (key: string) => string; +}; + +const PLAN_ACCENT: Record = { + free: '#737373', + pro: '#007AFF', + legend: '#f59e0b', +}; + +function SubscriptionSheet({ plan, colors, t }: SubscriptionSheetProps) { + const accentColor = PLAN_ACCENT[plan] ?? '#737373'; + const planLabel = + plan === 'legend' + ? t('settings.subscription_plan_legend') + : plan === 'pro' + ? t('settings.subscription_plan_pro') + : t('settings.subscription_plan_free'); + + return ( + + + + + {t('settings.subscription_sheet_title')} + + + + {planLabel} + + + + + + {t('settings.subscription_sheet_body')} + + + { + // TODO: für iOS-Submission ggf. zu nicht-tippbarem Text degradieren + // (Apple Guideline 3.1.1: externe Abo-Links können Review-Ablehnung triggern, + // wenn sie als Kauf-Umgehung gewertet werden. Standalone-URL ohne Preis-Info + // sollte ok sein, ist aber ungeprüft — bei Submission erneut prüfen.) + Linking.openURL('https://rebreak.org/account'); + }} + style={({ pressed }) => ({ + backgroundColor: accentColor, + borderRadius: 14, + paddingVertical: 14, + alignItems: 'center', + opacity: pressed ? 0.8 : 1, + })} + > + + {t('settings.subscription_sheet_cta')} + + + + ); +} + // ─── Settings Screen ─────────────────────────────────────────────────────── type PickerOption = { value: T; label: string }; @@ -66,6 +175,7 @@ export default function SettingsScreen() { // TrueSheet ref for Lyra-Voice picker (UISheetPresentationController bottom-sheet) const voiceSheetRef = useRef(null); + const subscriptionSheetRef = useRef(null); async function handleSignOut() { Alert.alert(t('auth.signOut'), '', [ @@ -182,7 +292,13 @@ export default function SettingsScreen() { icon: 'star-outline', label: t('settings.subscription'), sublabel: t('settings.subscription_desc'), - soon: true, + value: + plan === 'legend' + ? t('settings.subscription_plan_legend') + : plan === 'pro' + ? t('settings.subscription_plan_pro') + : t('settings.subscription_plan_free'), + onPress: () => subscriptionSheetRef.current?.present(), }, ], }, @@ -472,6 +588,16 @@ export default function SettingsScreen() { + + + +