diff --git a/apps/frontend/app/escrow/[id]/page.tsx b/apps/frontend/app/escrow/[id]/page.tsx index 9f8082e8..18280c03 100644 --- a/apps/frontend/app/escrow/[id]/page.tsx +++ b/apps/frontend/app/escrow/[id]/page.tsx @@ -5,6 +5,7 @@ import { useParams } from "next/navigation"; import Link from "next/link"; import { useEscrow } from "@/hooks/useEscrow"; import { useWallet } from "@/hooks/useWallet"; +import { useEscrowEvents } from "@/hooks/useEscrowEvents"; import EscrowHeader from "@/components/escrow/detail/EscrowHeader"; import PartiesSection from "@/components/escrow/detail/PartiesSection"; import TermsSection from "@/components/escrow/detail/TermsSection"; @@ -28,6 +29,37 @@ const EscrowDetailPage = () => { const [disputeOpen, setDisputeOpen] = useState(false); const [resolutionOpen, setResolutionOpen] = useState(false); const [dispute, setDispute] = useState(null); + const { status: escrowSocketStatus, onEscrowEvent } = useEscrowEvents(id as string); + + useEffect(() => { + const cleanupFunctions: Array<() => void> = []; + + cleanupFunctions.push( + onEscrowEvent('escrow:status_changed', () => refetch()), + ); + cleanupFunctions.push( + onEscrowEvent('escrow:funded', () => refetch()), + ); + cleanupFunctions.push( + onEscrowEvent('escrow:completed', () => refetch()), + ); + cleanupFunctions.push( + onEscrowEvent('escrow:dispute_filed', () => refetch()), + ); + cleanupFunctions.push( + onEscrowEvent('escrow:dispute_resolved', () => refetch()), + ); + + return () => { + cleanupFunctions.forEach((cleanup) => cleanup()); + }; + }, [id, onEscrowEvent, refetch]); + + useEffect(() => { + if (escrowSocketStatus === 'connected') { + refetch(); + } + }, [escrowSocketStatus, refetch]); useEffect(() => { if (escrow && publicKey) { diff --git a/apps/frontend/components/Providers.tsx b/apps/frontend/components/Providers.tsx index 823b5375..fc54d3db 100644 --- a/apps/frontend/components/Providers.tsx +++ b/apps/frontend/components/Providers.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ToastProvider } from '@/app/contexts/ToastProvider'; import { ThemeProvider } from '@/components/ThemeProvider'; +import { WebSocketProvider } from '@/providers/WebSocketProvider'; import { ErrorBoundary } from '@/components/ErrorBoundary'; export default function Providers({ children }: { children: React.ReactNode }) { @@ -20,6 +21,9 @@ export default function Providers({ children }: { children: React.ReactNode }) { + + {children} + {children} diff --git a/apps/frontend/components/common/NotificationBell.tsx b/apps/frontend/components/common/NotificationBell.tsx index c8c9da62..6f80388e 100644 --- a/apps/frontend/components/common/NotificationBell.tsx +++ b/apps/frontend/components/common/NotificationBell.tsx @@ -12,7 +12,7 @@ interface NotificationBellProps { const NotificationBell: React.FC = ({ className = '' }) => { const [isOpen, setIsOpen] = useState(false); - const { notifications, unreadCount, isLoading, markAsRead, markAllAsRead } = useNotifications(); + const { notifications, unreadCount, isLoading, connectionStatus, markAsRead, markAllAsRead } = useNotifications(); // Group notifications by date const groupedNotifications = notifications.reduce((acc, notification) => { @@ -99,8 +99,24 @@ const NotificationBell: React.FC = ({ className = '' }) = onClick={() => setIsOpen(!isOpen)} className="relative p-2 text-gray-300 hover:text-white transition-colors" aria-label="Notifications" + title={ + connectionStatus === 'connected' + ? 'Realtime updates active' + : connectionStatus === 'reconnecting' + ? 'Realtime reconnecting' + : 'Realtime disconnected' + } > + {unreadCount > 0 && ( {unreadCount > 99 ? '99+' : unreadCount} @@ -120,9 +136,27 @@ const NotificationBell: React.FC = ({ className = '' }) = {/* Panel */}
{/* Header */} -
-

Notifications

-
+
+
+

Notifications

+
+ + {connectionStatus === 'connected' + ? 'Live' + : connectionStatus === 'reconnecting' + ? 'Reconnecting' + : 'Disconnected'} +
+
+
{unreadCount > 0 && (