import React, { useState, useEffect, useRef } from 'react'; import { Bell } from 'lucide-react'; import { toast } from 'react-toastify'; import notificationService, { Notification } from '../services/notificationService'; import { formatDate } from '../../../shared/utils/format'; import useAuthStore from '../../../store/useAuthStore'; import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer'; const InAppNotificationBell: React.FC = () => { const { isAuthenticated, token, isLoading } = useAuthStore(); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [showDropdown, setShowDropdown] = useState(false); const [loading, setLoading] = useState(false); const intervalRef = useRef(null); const [isInitialized, setIsInitialized] = useState(false); // Wait for auth to initialize before checking React.useEffect(() => { // Small delay to ensure auth store is initialized const timer = setTimeout(() => { setIsInitialized(true); }, 100); return () => clearTimeout(timer); }, []); // Helper to check if user is actually authenticated (has valid token) const isUserAuthenticated = (): boolean => { // Don't check if still initializing if (isLoading || !isInitialized) { return false; } // Tokens are stored in httpOnly cookies, not localStorage // Only check authentication state from store return !!isAuthenticated; }; useEffect(() => { // Clear any existing interval first if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } // Early return if not authenticated - don't set up polling at all if (!isUserAuthenticated()) { setNotifications([]); setUnreadCount(0); return; } // Tokens are in httpOnly cookies, so we only check authentication state // No need to check for token in localStorage // Load notifications immediately loadNotifications(); // Poll for new notifications every 30 seconds, but only if authenticated intervalRef.current = setInterval(() => { // Re-check authentication on each poll const stillAuthenticated = isUserAuthenticated(); if (stillAuthenticated) { loadNotifications(); } else { // Clear interval and state if user becomes unauthenticated if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } setNotifications([]); setUnreadCount(0); } }, 30000); return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthenticated, token]); const loadNotifications = async () => { // Don't make API call if user is not authenticated // Tokens are in httpOnly cookies, so we only check authentication state if (!isAuthenticated) { // Clear state if not authenticated setNotifications([]); setUnreadCount(0); return; } try { const response = await notificationService.getMyNotifications({ status: 'delivered', limit: 10, }); const notifs = response.data.data || []; setNotifications(notifs); setUnreadCount(notifs.filter(n => !n.read_at).length); } catch (error) { // Silently fail } }; const handleMarkAsRead = async (notificationId: number) => { try { await notificationService.markAsRead(notificationId); setNotifications(notifications.map(n => n.id === notificationId ? { ...n, status: 'read' as Notification['status'], read_at: new Date().toISOString() } : n )); setUnreadCount(Math.max(0, unreadCount - 1)); } catch (error: unknown) { toast.error(getUserFriendlyError(error) || 'Failed to mark as read'); } }; const handleMarkAllAsRead = async () => { try { setLoading(true); const unread = notifications.filter(n => !n.read_at); await Promise.all(unread.map(n => notificationService.markAsRead(n.id))); setNotifications(notifications.map(n => ({ ...n, status: 'read' as Notification['status'], read_at: new Date().toISOString() }))); setUnreadCount(0); } catch (error: unknown) { toast.error(getUserFriendlyError(error) || 'Failed to mark all as read'); } finally { setLoading(false); } }; // Don't render if still initializing, not authenticated, or doesn't have a token if (isLoading || !isInitialized || !isUserAuthenticated()) { return null; } return (
{showDropdown && ( <>
setShowDropdown(false)} />

Notifications

{unreadCount > 0 && ( )}
{notifications.length === 0 ? (
No notifications
) : ( notifications.map((notification) => (
{ if (!notification.read_at) { handleMarkAsRead(notification.id); } }} >

{notification.subject || notification.notification_type.replace('_', ' ')}

{notification.content}

{formatDate(new Date(notification.created_at), 'short')}

{!notification.read_at && (
)}
)) )}
)}
); }; export default InAppNotificationBell;