feat(dev): switch plan-override to POST /api/dev/set-plan + add Settings debug row
debug.tsx: removed admin-403 special-case, calls /api/dev/set-plan directly. settings.tsx: new PlanPickerSheetContent (TrueSheet, DEV-only) in debug section with three plan options; uses same endpoint + invalidateMe(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5c9f3f687f
commit
7369912d60
@ -137,23 +137,13 @@ function PlanOverrideToggle({
|
||||
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 }),
|
||||
await apiFetch('/api/dev/set-plan', {
|
||||
method: 'POST',
|
||||
body: { 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);
|
||||
}
|
||||
Alert.alert('Fehler', e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -196,7 +186,7 @@ function PlanOverrideToggle({
|
||||
lineHeight: 17,
|
||||
}}
|
||||
>
|
||||
PATCH /api/admin/users/:id — braucht Admin-Rechte
|
||||
POST /api/dev/set-plan — nur staging
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
ScrollView,
|
||||
@ -20,6 +21,8 @@ import { useAuthStore } from '../stores/auth';
|
||||
import { useThemeStore, type ThemeMode } from '../stores/theme';
|
||||
import { useLanguageStore, type AppLanguage } from '../stores/language';
|
||||
import { useUserPlan } from '../hooks/useUserPlan';
|
||||
import { useMe, invalidateMe, type Plan } from '../hooks/useMe';
|
||||
import { apiFetch } from '../lib/api';
|
||||
import { AppHeader } from '../components/AppHeader';
|
||||
|
||||
// ─── Subscription Sheet ────────────────────────────────────────────────────
|
||||
@ -173,9 +176,12 @@ export default function SettingsScreen() {
|
||||
// For now: picker is wired to local state only, changes are NOT persisted.
|
||||
const [selectedVoice, setSelectedVoice] = useState('EXAVITQu4vr4xnSDxMaL');
|
||||
|
||||
const { me } = useMe();
|
||||
|
||||
// TrueSheet ref for Lyra-Voice picker (UISheetPresentationController bottom-sheet)
|
||||
const voiceSheetRef = useRef<TrueSheet>(null);
|
||||
const subscriptionSheetRef = useRef<TrueSheet>(null);
|
||||
const planSheetRef = useRef<TrueSheet>(null);
|
||||
|
||||
async function handleSignOut() {
|
||||
Alert.alert(t('auth.signOut'), '', [
|
||||
@ -360,6 +366,13 @@ export default function SettingsScreen() {
|
||||
sublabel: t('settings.debug_tts_desc'),
|
||||
soon: true,
|
||||
},
|
||||
{
|
||||
icon: 'star-outline',
|
||||
label: t('settings.debug_plan'),
|
||||
sublabel: t('settings.debug_plan_desc'),
|
||||
value: me?.plan ?? '…',
|
||||
onPress: () => planSheetRef.current?.present(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
@ -596,6 +609,23 @@ export default function SettingsScreen() {
|
||||
<SubscriptionSheet plan={plan} colors={colors} t={t} />
|
||||
</TrueSheet>
|
||||
|
||||
{__DEV__ && (
|
||||
<TrueSheet
|
||||
ref={planSheetRef}
|
||||
detents={['auto']}
|
||||
cornerRadius={20}
|
||||
grabber
|
||||
backgroundColor={colors.surface}
|
||||
>
|
||||
<PlanPickerSheetContent
|
||||
currentPlan={me?.plan ?? 'free'}
|
||||
colors={colors}
|
||||
t={t}
|
||||
onDone={() => planSheetRef.current?.dismiss()}
|
||||
/>
|
||||
</TrueSheet>
|
||||
)}
|
||||
|
||||
<TrueSheet
|
||||
ref={voiceSheetRef}
|
||||
detents={['auto', 1]}
|
||||
@ -667,3 +697,97 @@ export default function SettingsScreen() {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Plan-Override Sheet (DEV only) ───────────────────────────────────────
|
||||
|
||||
const DEV_PLANS: Plan[] = ['free', 'pro', 'legend'];
|
||||
|
||||
const DEV_PLAN_ACCENT: Record<Plan, string> = {
|
||||
free: '#737373',
|
||||
pro: '#007AFF',
|
||||
legend: '#f59e0b',
|
||||
};
|
||||
|
||||
function PlanPickerSheetContent({
|
||||
currentPlan,
|
||||
colors,
|
||||
t,
|
||||
onDone,
|
||||
}: {
|
||||
currentPlan: Plan;
|
||||
colors: import('../lib/theme').ColorScheme;
|
||||
t: (key: string) => string;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function pick(plan: Plan) {
|
||||
if (plan === currentPlan || loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await apiFetch('/api/dev/set-plan', { method: 'POST', body: { plan } });
|
||||
invalidateMe();
|
||||
onDone();
|
||||
} catch (e: unknown) {
|
||||
Alert.alert(t('common.error'), e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 32, backgroundColor: colors.surface }}>
|
||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text, marginBottom: 4 }}>
|
||||
{t('settings.debug_plan')}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, marginBottom: 20, lineHeight: 17 }}>
|
||||
{t('settings.debug_plan_desc')}
|
||||
</Text>
|
||||
{DEV_PLANS.map((plan, idx) => {
|
||||
const isActive = plan === currentPlan;
|
||||
const accent = DEV_PLAN_ACCENT[plan];
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={plan}
|
||||
onPress={() => pick(plan)}
|
||||
disabled={loading || isActive}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: idx < DEV_PLANS.length - 1 ? 1 : 0,
|
||||
borderBottomColor: colors.border,
|
||||
opacity: loading && !isActive ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
||||
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: colors.text,
|
||||
fontFamily: isActive ? 'Nunito_700Bold' : 'Nunito_400Regular',
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{plan}
|
||||
</Text>
|
||||
</View>
|
||||
{isActive ? (
|
||||
loading ? (
|
||||
<ActivityIndicator size="small" color={accent} />
|
||||
) : (
|
||||
<Ionicons name="checkmark" size={20} color={accent} />
|
||||
)
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user