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

71 lines
1.7 KiB
TypeScript

import { create } from 'zustand';
import { apiFetch } from '../lib/api';
import { getDeviceInfo } from '../lib/deviceId';
export interface UserDevice {
id: string;
deviceId: string;
platform: string;
model: string | null;
name: string | null;
lastSeenAt: string;
createdAt: string;
isCurrent?: boolean;
}
type DevicesState = {
devices: UserDevice[];
maxDevices: number;
plan: string;
loading: boolean;
ensureRegistered: () => Promise<void>;
loadDevices: () => Promise<void>;
removeDevice: (id: string) => Promise<void>;
};
export const useDevicesStore = create<DevicesState>((set) => ({
devices: [],
maxDevices: 1,
plan: 'free',
loading: false,
ensureRegistered: async () => {
const info = await getDeviceInfo().catch(() => null);
if (!info) return;
await apiFetch('/api/devices/register', {
method: 'POST',
skipDeviceHeader: true,
body: {
deviceId: info.deviceId,
platform: info.platform,
name: info.name,
model: info.model,
osVersion: info.osVersion,
appVersion: info.appVersion,
},
}).then((res: any) => {
set({ maxDevices: res.max ?? 1 });
}).catch(() => {});
},
loadDevices: async () => {
set({ loading: true });
try {
await useDevicesStore.getState().ensureRegistered();
const res = await apiFetch<{ devices: UserDevice[]; max: number; plan: string }>(
'/api/devices'
);
set({ devices: res.devices, maxDevices: res.max, plan: res.plan });
} finally {
set({ loading: false });
}
},
removeDevice: async (id: string) => {
await apiFetch(`/api/devices/${id}`, { method: 'DELETE' });
set((s) => ({ devices: s.devices.filter((d) => d.id !== id) }));
},
}));