import { defineStore } from "pinia"; import { ref } from "vue"; import { getCurrentUser } from "@/lib/api"; import { toast } from "vue-sonner"; export const useAuthStore = defineStore("auth", () => { const user = ref(null); const isLoading = ref(true); const showCompleteProfile = ref(false); let initialized = false; const wsAuthFailed = ref(false); async function refreshUser() { try { const userData = await getCurrentUser(); user.value = userData; wsAuthFailed.value = false; // Reset on successful auth showCompleteProfile.value = !!( userData && (!userData.phone || !userData.shipping_address) ); } catch { user.value = null; wsAuthFailed.value = true; showCompleteProfile.value = false; stopPing(); } finally { isLoading.value = false; if (user.value && !pingInterval && !wsAuthFailed.value) startPing(); } } let pingInterval: number | null = null; const unreadMessagesCount = ref(0); let globalWs: WebSocket | null = null; let reconnectTimer: number | null = null; const WS_BASE_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8000"; // ── Audio ────────────────────────────────────────────────────────────────── // Single shared AudioContext, created lazily on first user gesture. let sharedAudioCtx: AudioContext | null = null; function getAudioCtx(): AudioContext | null { const AudioCtx = window.AudioContext || (window as any).webkitAudioContext; if (!AudioCtx) return null; if (!sharedAudioCtx) sharedAudioCtx = new AudioCtx(); return sharedAudioCtx; } // Unlock AudioContext as soon as the user interacts with the page function unlockAudio() { const ctx = getAudioCtx(); if (ctx && ctx.state === 'suspended') ctx.resume().catch(() => {}); } window.addEventListener('click', unlockAudio, { passive: true }); window.addEventListener('keydown', unlockAudio, { passive: true }); window.addEventListener('touchstart', unlockAudio, { passive: true }); async function playUpdateSound() { try { const ctx = getAudioCtx(); if (!ctx) return; if (ctx.state === 'suspended') await ctx.resume(); const osc = ctx.createOscillator(); const gainNode = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(180, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(140, ctx.currentTime + 0.3); gainNode.gain.setValueAtTime(0, ctx.currentTime); gainNode.gain.linearRampToValueAtTime(0.15, ctx.currentTime + 0.05); gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.35); osc.connect(gainNode); gainNode.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.35); } catch (e) { console.warn("Audio update sound error", e); } } async function playNotificationSound() { try { const ctx = getAudioCtx(); if (!ctx) return; if (ctx.state === 'suspended') await ctx.resume(); const osc = ctx.createOscillator(); const gainNode = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(880, ctx.currentTime); // A5 osc.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.1); // Up to A6 gainNode.gain.setValueAtTime(0, ctx.currentTime); gainNode.gain.linearRampToValueAtTime(0.1, ctx.currentTime + 0.05); gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.2); osc.connect(gainNode); gainNode.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.2); } catch (e) { console.warn("Audio notification sound error", e); } } async function playChatSound() { try { const ctx = getAudioCtx(); if (!ctx) return; if (ctx.state === 'suspended') await ctx.resume(); const osc = ctx.createOscillator(); const gainNode = ctx.createGain(); osc.type = "sine"; osc.frequency.setValueAtTime(880, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.1); gainNode.gain.setValueAtTime(0, ctx.currentTime); gainNode.gain.linearRampToValueAtTime(0.3, ctx.currentTime + 0.05); gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3); osc.connect(gainNode); gainNode.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.3); } catch (e) { console.warn("Audio chat sound error", e); } } // ─────────────────────────────────────────────────────────────────────────── function startPing() { stopPing(); // Ensure fresh start const token = localStorage.getItem("token"); if (!token || wsAuthFailed.value) return; globalWs = new WebSocket(`${WS_BASE_URL}/global?token=${encodeURIComponent(token)}`); globalWs.onopen = () => { // Send ping every 25 seconds to keep connection alive and update online status in Redis pingInterval = window.setInterval(() => { if (globalWs && globalWs.readyState === WebSocket.OPEN) { globalWs.send("ping"); } }, 25000); }; globalWs.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === "unread_count" && msg.count !== undefined) { if (msg.count > unreadMessagesCount.value) { playNotificationSound(); } unreadMessagesCount.value = msg.count; } else if (msg.type === "new_chat_message") { toast(`Message for Order #${msg.order_id}`, { description: msg.text, action: { label: "View", onClick: () => { window.location.href = `/admin?order=${msg.order_id}`; } } }); } else if (msg.type === "account_suspended") { toast.error("Your account has been suspended by an administrator.", { duration: 10000 }); logout(); } else if (msg.type === "order_read") { window.dispatchEvent(new CustomEvent("radionica:order_read", { detail: { order_id: msg.order_id } })); } else if (msg.type === "order_updated") { playUpdateSound(); window.dispatchEvent(new CustomEvent("radionica:order_updated", { detail: { order_id: msg.order_id } })); } } catch (e) { console.error("WS Parse error", e); } }; globalWs.onclose = (event) => { if (pingInterval) clearInterval(pingInterval); // 4001: Auth fail, 4003: Suspended, 1008: Policy if (event.code === 4001 || event.code === 1008 || event.code === 4003) { console.warn(`WS Connection terminated (code ${event.code}).`); wsAuthFailed.value = true; if (event.code === 4003) { user.value = null; // Instant clear localStorage.removeItem("token"); } return; } if (user.value && !wsAuthFailed.value) { // If still logged in and no auth error, try reconnecting reconnectTimer = window.setTimeout(startPing, 5000); } }; globalWs.onerror = () => { // On error, the close event will follow console.error("WS Connection error occurred"); }; } function stopPing() { if (pingInterval) { clearInterval(pingInterval); pingInterval = null; } if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } if (globalWs) { globalWs.onclose = null; // Disable auto reconnect globalWs.onerror = null; globalWs.close(); globalWs = null; } } let initPromise: Promise | null = null; function init(): Promise { if (!initPromise) { initPromise = refreshUser(); } return initPromise; } function setUser(u: any) { user.value = u; wsAuthFailed.value = false; if (u && !pingInterval) startPing(); } function onProfileComplete() { showCompleteProfile.value = false; refreshUser(); } async function logout() { const { logoutUser } = await import("@/lib/api"); try { await logoutUser(); } catch (e) { console.error("Logout API call failed, continuing local cleanup", e); } localStorage.removeItem("token"); user.value = null; unreadMessagesCount.value = 0; stopPing(); } async function refreshUnreadCount() { // Left empty or removed, as WS handles updates now. // If you need manual force, you could re-trigger connect or add an endpoint, // but the WS pushes update automatically. } return { user, isLoading, showCompleteProfile, unreadMessagesCount, init, setUser, refreshUser, onProfileComplete, logout, refreshUnreadCount, playChatSound }; });