Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail. GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift. - KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding - react-native-keyboard-controller installiert + KeyboardProvider in Root - Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO) - Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn - DPad-Buttons 60→48, more bg, no scale - useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates - dm.tsx: resolveAvatar wrap (iron.png-Warning) - Mail-error-humanizer + locales Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
2.5 KiB
TypeScript
90 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;
|
|
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();
|
|
},
|
|
};
|
|
}
|