import { useEffect, useRef, useState } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; import type { JoinResponse, SessionPreview, SessionSnapshot } from "../shared/types"; import { getApiBaseUrl, getWsUrl } from "../config/api"; import { tStatic } from "../i18n"; const STORAGE_KEY = "negopoly:session"; type StoredSession = { sessionId: string; sessionCode: string; playerId: string; }; async function readStoredSession(): Promise { try { const raw = await AsyncStorage.getItem(STORAGE_KEY); if (!raw) return null; return JSON.parse(raw) as StoredSession; } catch { return null; } } async function writeStoredSession(session: StoredSession) { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(session)); } async function clearStoredSession() { await AsyncStorage.removeItem(STORAGE_KEY); } export function useSessionManager() { const [sessionId, setSessionId] = useState(""); const [sessionCode, setSessionCode] = useState(""); const [playerId, setPlayerId] = useState(""); const [session, setSession] = useState(null); const [error, setError] = useState(null); const [pendingTakeoverId, setPendingTakeoverId] = useState(null); const [connectionState, setConnectionState] = useState< "idle" | "connecting" | "open" | "error" >("idle"); const [tick, setTick] = useState(0); const wsRef = useRef(null); useEffect(() => { let mounted = true; readStoredSession().then((stored) => { if (!mounted || !stored) return; setSessionId(stored.sessionId); setSessionCode(stored.sessionCode); setPlayerId(stored.playerId); }); return () => { mounted = false; }; }, []); useEffect(() => { const timer = setInterval(() => setTick((value) => value + 1), 1000); return () => clearInterval(timer); }, []); useEffect(() => { if (!sessionId || !playerId) { setConnectionState("idle"); setSession(null); return; } setConnectionState("connecting"); const ws = new WebSocket(getWsUrl(sessionId, playerId)); wsRef.current = ws; ws.onopen = () => setConnectionState("open"); ws.onmessage = (event) => { try { const message = JSON.parse(event.data); if (message.type === "state") { setSession(message.session as SessionSnapshot); } if (message.type === "error") { setError(message.message); } if (message.type === "takeover_approved") { const assignedId = message.assignedPlayerId as string; setPlayerId(assignedId); if (sessionId && sessionCode) { writeStoredSession({ sessionId, sessionCode, playerId: assignedId, }); } } } catch { setError(tStatic("error.parseResponse")); } }; ws.onerror = () => setConnectionState("error"); ws.onclose = (event) => { setConnectionState("error"); const reason = typeof event?.reason === "string" ? event.reason : ""; if (event?.code === 1008 && /session not found|player not found/i.test(reason)) { resetSession(); } }; const pingTimer = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "ping", sessionId, playerId })); } }, 15000); return () => { clearInterval(pingTimer); ws.close(); }; }, [sessionId, playerId]); useEffect(() => { if (!pendingTakeoverId) return; if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; if (!sessionId || !playerId) return; wsRef.current.send( JSON.stringify({ type: "takeover_request", sessionId, playerId, dummyId: pendingTakeoverId, }), ); setPendingTakeoverId(null); }, [pendingTakeoverId, sessionId, playerId]); async function createSession(bankerName: string) { setError(null); setSession(null); const response = await fetch(`${getApiBaseUrl()}/api/session`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ bankerName }), }); if (!response.ok) { setError(tStatic("error.createSession")); return null; } const data = (await response.json()) as JoinResponse; setSessionId(data.sessionId); setSessionCode(data.sessionCode); setPlayerId(data.playerId); await writeStoredSession({ sessionId: data.sessionId, sessionCode: data.sessionCode, playerId: data.playerId, }); return data; } async function joinSession(code: string, name: string) { setError(null); setSession(null); if (!code) { setError(tStatic("entry.alert.enterCode")); return null; } const storedNow = await readStoredSession(); const reusePlayerId = storedNow && (storedNow.sessionCode === code || storedNow.sessionId === code) ? storedNow.playerId : undefined; const response = await fetch(`${getApiBaseUrl()}/api/session/${code}/join`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, playerId: reusePlayerId }), }); if (!response.ok) { setError(tStatic("error.joinSession")); return null; } const data = (await response.json()) as JoinResponse; setSessionId(data.sessionId); setSessionCode(data.sessionCode); setPlayerId(data.playerId); await writeStoredSession({ sessionId: data.sessionId, sessionCode: data.sessionCode, playerId: data.playerId, }); return data; } async function fetchSessionPreview(code: string): Promise { if (!code) return null; const response = await fetch(`${getApiBaseUrl()}/api/session/${code}/info`); if (!response.ok) { setError(tStatic("error.loadSessionInfo")); return null; } return (await response.json()) as SessionPreview; } function sendMessage(payload: Record) { if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { setError(tStatic("error.connectionNotReady")); return; } wsRef.current.send(JSON.stringify(payload)); } async function resetSession() { try { await clearStoredSession(); } catch { // Ignore storage errors on reset. } setSessionId(""); setSessionCode(""); setPlayerId(""); setSession(null); setError(null); setPendingTakeoverId(null); setConnectionState("idle"); } async function leaveSession() { const ws = wsRef.current; wsRef.current = null; if (ws && ws.readyState !== WebSocket.CLOSED) { try { ws.close(); } catch { // Ignore failures while closing the socket. } } await resetSession(); } const me = session?.players.find((player) => player.id === playerId) ?? null; const isBanker = me?.role === "banker"; return { sessionId, sessionCode, playerId, session, me, isBanker, tick, error, connectionState, setError, createSession, joinSession, fetchSessionPreview, sendMessage, resetSession, leaveSession, setSessionId, setPlayerId, setSession, setPendingTakeoverId, }; }