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>
71 lines
1.7 KiB
TypeScript
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) }));
|
|
},
|
|
}));
|