chahinebrini d9bb7ef91a feat(native): lyra voice picker UI + me-hydration
- settings.tsx: neuer Abschnitt 'Lyra (Legend)' — nur sichtbar wenn plan==='legend',
  UIMenu mit 3 Optionen (Standard / Stimme 1 / Stimme 2), chevron-forward Anchor.
  Optimistic Update via PATCH /api/profile/me/lyra-voice, Revert bei Error.
- useMe.ts: lyraVoiceId im Me-Type — Hydration aus /api/auth/me beim App-Start.
- de.json + en.json: settings.lyra_voice + lyra_voice_default/_1/_2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:15:49 +02:00

91 lines
2.5 KiB
TypeScript

import { useEffect, useState } from 'react';
import { apiFetch } from '../lib/api';
export type Plan = 'free' | 'pro' | 'legend';
/**
* Single source of truth für den eingeloggten User. /api/auth/me joint
* `auth.users` mit `rebreak.profiles` server-side — wir bekommen alles in
* einem Request: plan, avatar, nickname, streak.
*
* Live-Update-Pattern (siehe RECOVERY_LOG): nach Profile-Edit (PATCH /api/auth/me)
* MUSS `invalidateMe()` aufgerufen werden — alle useMe-Konsumenten (AppHeader,
* PostCard, ComposeCard, etc.) re-fetchen automatisch via Listener-Subscribe.
*
* WICHTIG: nicht aus `supabase.auth.getUser().user_metadata` lesen — das
* sind nur die JWT-Claims vom Signup-Zeitpunkt, NICHT der aktuelle Profile-
* Stand (Avatar/Nickname/Plan werden via Profile-Edit-API geupdated, landen
* in der DB, NICHT zurück ins JWT-Claim).
*/
export type Me = {
id: string;
email: string;
username: string;
nickname: string | null;
avatar: string | null;
plan: Plan;
streak: number;
lyraVoiceId: string | null;
created_at?: string;
};
let cachedMe: Me | null = null;
const listeners = new Set<() => void>();
/**
* Lädt /api/auth/me neu und benachrichtigt ALLE useMe-Konsumenten in der App.
* Nach jedem PATCH /api/auth/me aufrufen — sonst sehen Konsumenten alten Cache.
*/
export function invalidateMe(): void {
cachedMe = null;
for (const cb of listeners) cb();
}
export function useMe(): { me: Me | null; loading: boolean; reload: () => void } {
const [me, setMe] = useState<Me | null>(cachedMe);
const [loading, setLoading] = useState(cachedMe === null);
const [version, setVersion] = useState(0);
// Auf globale Invalidierung lauschen (Avatar-/Nickname-Update aus Profile-Edit)
useEffect(() => {
const cb = () => setVersion((v) => v + 1);
listeners.add(cb);
return () => {
listeners.delete(cb);
};
}, []);
useEffect(() => {
let cancelled = false;
(async () => {
// Falls cache schon frisch ist (von anderem Konsumenten gerade geladen): nutzen
if (cachedMe !== null) {
setMe(cachedMe);
setLoading(false);
return;
}
try {
const res = await apiFetch<Me>('/api/auth/me');
if (cancelled) return;
cachedMe = res;
setMe(res);
} catch (e) {
console.warn('[useMe] fetch failed:', e);
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [version]);
return {
me,
loading,
reload: () => {
invalidateMe();
},
};
}