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;
|
if (plan === currentPlan) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// PATCH /api/admin/users/:id requires admin privileges.
|
await apiFetch('/api/dev/set-plan', {
|
||||||
// If the dev-user is not admin, this returns 403 — see alert below.
|
method: 'POST',
|
||||||
await apiFetch(`/api/admin/users/${userId}`, {
|
body: { plan },
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify({ plan }),
|
|
||||||
});
|
});
|
||||||
invalidateMe();
|
invalidateMe();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
Alert.alert('Fehler', 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -196,7 +186,7 @@ function PlanOverrideToggle({
|
|||||||
lineHeight: 17,
|
lineHeight: 17,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
PATCH /api/admin/users/:id — braucht Admin-Rechte
|
POST /api/dev/set-plan — nur staging
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
Linking,
|
Linking,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
@ -20,6 +21,8 @@ import { useAuthStore } from '../stores/auth';
|
|||||||
import { useThemeStore, type ThemeMode } from '../stores/theme';
|
import { useThemeStore, type ThemeMode } from '../stores/theme';
|
||||||
import { useLanguageStore, type AppLanguage } from '../stores/language';
|
import { useLanguageStore, type AppLanguage } from '../stores/language';
|
||||||
import { useUserPlan } from '../hooks/useUserPlan';
|
import { useUserPlan } from '../hooks/useUserPlan';
|
||||||
|
import { useMe, invalidateMe, type Plan } from '../hooks/useMe';
|
||||||
|
import { apiFetch } from '../lib/api';
|
||||||
import { AppHeader } from '../components/AppHeader';
|
import { AppHeader } from '../components/AppHeader';
|
||||||
|
|
||||||
// ─── Subscription Sheet ────────────────────────────────────────────────────
|
// ─── Subscription Sheet ────────────────────────────────────────────────────
|
||||||
@ -173,9 +176,12 @@ export default function SettingsScreen() {
|
|||||||
// For now: picker is wired to local state only, changes are NOT persisted.
|
// For now: picker is wired to local state only, changes are NOT persisted.
|
||||||
const [selectedVoice, setSelectedVoice] = useState('EXAVITQu4vr4xnSDxMaL');
|
const [selectedVoice, setSelectedVoice] = useState('EXAVITQu4vr4xnSDxMaL');
|
||||||
|
|
||||||
|
const { me } = useMe();
|
||||||
|
|
||||||
// TrueSheet ref for Lyra-Voice picker (UISheetPresentationController bottom-sheet)
|
// TrueSheet ref for Lyra-Voice picker (UISheetPresentationController bottom-sheet)
|
||||||
const voiceSheetRef = useRef<TrueSheet>(null);
|
const voiceSheetRef = useRef<TrueSheet>(null);
|
||||||
const subscriptionSheetRef = useRef<TrueSheet>(null);
|
const subscriptionSheetRef = useRef<TrueSheet>(null);
|
||||||
|
const planSheetRef = useRef<TrueSheet>(null);
|
||||||
|
|
||||||
async function handleSignOut() {
|
async function handleSignOut() {
|
||||||
Alert.alert(t('auth.signOut'), '', [
|
Alert.alert(t('auth.signOut'), '', [
|
||||||
@ -360,6 +366,13 @@ export default function SettingsScreen() {
|
|||||||
sublabel: t('settings.debug_tts_desc'),
|
sublabel: t('settings.debug_tts_desc'),
|
||||||
soon: true,
|
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} />
|
<SubscriptionSheet plan={plan} colors={colors} t={t} />
|
||||||
</TrueSheet>
|
</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
|
<TrueSheet
|
||||||
ref={voiceSheetRef}
|
ref={voiceSheetRef}
|
||||||
detents={['auto', 1]}
|
detents={['auto', 1]}
|
||||||
@ -667,3 +697,97 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</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