chahinebrini e9d34dbe78 feat(settings): subscription section + __DEV__ plan-override toggle
- 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) <noreply@anthropic.com>
2026-05-11 14:13:47 +02:00

302 lines
8.1 KiB
TypeScript

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__) {
router.replace('/');
}
}, [router]);
if (!__DEV__) {
return <View style={{ flex: 1, backgroundColor: colors.bg }} />;
}
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
<View
style={{
paddingHorizontal: 12,
paddingTop: 4,
paddingBottom: 12,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.06)',
}}
>
<Pressable
onPress={() => router.back()}
hitSlop={8}
style={({ pressed }) => ({
opacity: pressed ? 0.6 : 1,
})}
>
<View style={{
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
}}>
<Ionicons name="chevron-back" size={26} color={colors.text} />
</View>
</Pressable>
<Text style={{ fontSize: 20, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
Debug
</Text>
</View>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 60 }}
showsVerticalScrollIndicator={false}
>
<View
style={{
backgroundColor: '#fef3c7',
borderRadius: 14,
padding: 14,
marginBottom: 20,
borderWidth: 1,
borderColor: '#fde68a',
flexDirection: 'row',
gap: 10,
alignItems: 'flex-start',
}}
>
<Ionicons name="warning-outline" size={20} color="#b45309" style={{ marginTop: 1 }} />
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#78350f' }}>
Dev only
</Text>
<Text
style={{
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: '#92400e',
marginTop: 4,
lineHeight: 17,
}}
>
Diese Page ist nur in __DEV__ verfügbar. Production-Builds redirecten auf /.
</Text>
</View>
</View>
{me ? (
<PlanOverrideToggle
colors={colors}
userId={me.id}
currentPlan={me.plan}
/>
) : null}
<DebugStub
title="LLM-Provider Toggle"
subtitle="Phase C: aus app/urge.tsx hierher migrieren"
icon="bulb-outline"
/>
<DebugStub
title="TTS-Provider Toggle"
subtitle="Phase C: aus app/urge.tsx hierher migrieren"
icon="volume-high-outline"
/>
<DebugStub
title="Bench-Output"
subtitle="TTS/LLM-Latenz-Logs anzeigen (TODO)"
icon="speedometer-outline"
/>
</ScrollView>
</SafeAreaView>
);
}
const PLANS: Plan[] = ['free', 'pro', 'legend'];
const PLAN_COLOR: Record<Plan, string> = {
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 (
<View
style={{
backgroundColor: colors.surface,
borderRadius: 14,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
padding: 14,
marginBottom: 12,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 }}>
<View
style={{
width: 36,
height: 36,
borderRadius: 11,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Ionicons name="star-outline" size={18} color={colors.textMuted} />
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
Plan-Override (DEV)
</Text>
<Text
style={{
fontSize: 12,
color: colors.textMuted,
fontFamily: 'Nunito_400Regular',
marginTop: 3,
lineHeight: 17,
}}
>
PATCH /api/admin/users/:id braucht Admin-Rechte
</Text>
</View>
</View>
<View style={{ flexDirection: 'row', gap: 8 }}>
{PLANS.map((plan) => {
const isActive = plan === currentPlan;
const accent = PLAN_COLOR[plan];
return (
<Pressable
key={plan}
onPress={() => 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,
})}
>
<Text
style={{
fontSize: 13,
fontFamily: 'Nunito_700Bold',
color: isActive ? '#ffffff' : colors.textMuted,
textTransform: 'capitalize',
}}
>
{plan}
</Text>
</Pressable>
);
})}
</View>
</View>
);
}
function DebugStub({
title,
subtitle,
icon,
}: {
title: string;
subtitle: string;
icon: React.ComponentProps<typeof Ionicons>['name'];
}) {
const colors = useColors();
return (
<View
style={{
backgroundColor: colors.surface,
borderRadius: 14,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
padding: 14,
marginBottom: 12,
flexDirection: 'row',
gap: 12,
alignItems: 'flex-start',
opacity: 0.6,
}}
>
<View
style={{
width: 36,
height: 36,
borderRadius: 11,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Ionicons name={icon} size={18} color={colors.textMuted} />
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_700Bold' }}>{title}</Text>
<Text
style={{
fontSize: 12,
color: colors.textMuted,
fontFamily: 'Nunito_400Regular',
marginTop: 3,
lineHeight: 17,
}}
>
{subtitle}
</Text>
</View>
</View>
);
}