chahinebrini 5d6c322129 wip: KeyboardAwareSheet migrations + Snake/Tetris UI + iron.png + useMe live-update
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>
2026-05-10 23:59:25 +02:00

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();
},
};
}