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() {
+
+
+
+