'use client'; import { useEffect, useRef, useCallback } from 'react'; import { io, type Socket } from 'socket.io-client'; import { toast } from 'sonner'; import { useAuthStore } from '@/lib/auth-store'; import type { NotificationDto } from '@/lib/notifications-api'; import { useNotificationsStore } from '@/lib/notifications-store'; /** Base URL for the Socket.IO server (without namespace). */ const SOCKET_URL = process.env['NEXT_PUBLIC_API_URL']?.replace('/api/v1', '') || 'http://localhost:3001'; /** * Hook that manages the Socket.IO connection for real-time notifications. * * Connects to the `/notifications` namespace on the backend * {@link NotificationsGateway} with JWT auth handshake. * * - Authenticates via `auth.token` (access-token from cookie or store) * - Listens for `notification:new` → adds to store + shows toast * - Listens for `notification:unread-count` → syncs badge count * - Auto-reconnects with exponential backoff (1 s → 10 s) * - Disconnects on logout */ export function useSocketNotifications() { const socketRef = useRef(null); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const { addNotification, incrementUnread, setUnreadCount } = useNotificationsStore(); /** Extract the access-token cookie value (if present). */ const getAccessToken = useCallback((): string | undefined => { if (typeof document === 'undefined') return undefined; const match = document.cookie .split('; ') .find((c) => c.startsWith('goodgo_access_token=')); return match?.split('=')[1]; }, []); useEffect(() => { if (!isAuthenticated) { if (socketRef.current) { socketRef.current.disconnect(); socketRef.current = null; } return; } // Don't create duplicate connections if (socketRef.current?.connected) return; const token = getAccessToken(); const socket = io(`${SOCKET_URL}/notifications`, { path: '/socket.io', auth: token ? { token } : undefined, withCredentials: true, // Also send httpOnly cookies as fallback transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: Infinity, reconnectionDelay: 1000, reconnectionDelayMax: 10000, autoConnect: true, }); socket.on('connect', () => { // Connection established — unread count arrives via notification:unread-count }); socket.on('notification:new', (data: NotificationDto) => { addNotification(data); incrementUnread(); // Show a sonner toast for the incoming notification toast(data.title ?? 'Thông báo mới', { description: data.body, duration: 5000, }); }); socket.on( 'notification:unread-count', (data: { unreadCount: number }) => { setUnreadCount(data.unreadCount); }, ); socket.on('disconnect', (reason) => { // Socket.IO auto-reconnects for transport errors. // Only server-initiated disconnects need explicit reconnect. if (reason === 'io server disconnect') { socket.connect(); } }); socket.on('connect_error', (err) => { console.warn('[ws] connection error:', err.message); }); socketRef.current = socket; return () => { socket.disconnect(); socketRef.current = null; }; }, [isAuthenticated, addNotification, incrementUnread, setUnreadCount, getAccessToken]); }