Files
dmail-api/FRONTEND_WEBSOCKET_INTEGRATION.md
T
2025-07-19 15:40:29 +03:30

20 KiB

📧 Email WebSocket Integration Guide for React

This guide will help you integrate real-time email notifications into your React application using WebSocket connections.

🚀 Quick Start

Installation

npm install socket.io-client
# or
yarn add socket.io-client

Basic Setup

import io from "socket.io-client";

const socket = io("ws://localhost:3000/email", {
  auth: {
    token: localStorage.getItem("jwt-token"), // Your JWT authentication token
  },
});

🔌 Connection Setup

1. Basic Connection Hook

// hooks/useEmailWebSocket.ts
import { useEffect, useState } from "react";
import io, { Socket } from "socket.io-client";

interface UseEmailWebSocketOptions {
  token: string;
  serverUrl?: string;
  onConnect?: () => void;
  onDisconnect?: () => void;
  onError?: (error: string) => void;
}

export const useEmailWebSocket = ({ token, serverUrl = "ws://localhost:3000/email", onConnect, onDisconnect, onError }: UseEmailWebSocketOptions) => {
  const [socket, setSocket] = useState<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    if (!token) return;

    const newSocket = io(serverUrl, {
      auth: { token },
      transports: ["websocket", "polling"],
    });

    // Connection events
    newSocket.on("connect", () => {
      console.log("✅ Connected to email WebSocket");
      setIsConnected(true);
      onConnect?.();
    });

    newSocket.on("disconnect", () => {
      console.log("❌ Disconnected from email WebSocket");
      setIsConnected(false);
      onDisconnect?.();
    });

    newSocket.on("connection_established", (data) => {
      console.log("🎉 Email WebSocket connection established:", data);
    });

    newSocket.on("connection_error", (error) => {
      console.error("💥 Email WebSocket connection error:", error);
      onError?.(error.error);
    });

    // Health monitoring
    newSocket.on("ping", () => {
      newSocket.emit("pong", { timestamp: new Date().toISOString() });
    });

    setSocket(newSocket);

    return () => {
      newSocket.close();
      setSocket(null);
    };
  }, [token, serverUrl]);

  return { socket, isConnected };
};

2. Email Events Hook

// hooks/useEmailEvents.ts
import { useEffect, useState } from "react";
import { Socket } from "socket.io-client";

// TypeScript interfaces (copy from your backend)
export interface EmailPayload {
  messageId: number;
  userId: string;
  from: {
    name?: string;
    address: string;
  };
  to: Array<{
    name?: string;
    address: string;
  }>;
  subject: string;
  preview?: string;
  hasAttachments: boolean;
  timestamp: string;
  mailboxId: string;
  mailboxName: string;
  isRead: boolean;
  isFlagged: boolean;
  size: number;
}

export interface EmailStatusPayload {
  messageId: number;
  userId: string;
  mailboxId: string;
  timestamp: string;
}

export interface UnreadCountPayload {
  userId: string;
  totalUnread: number;
  mailboxCounts: Record<string, number>;
  timestamp: string;
  count: number;
}

interface EmailEventHandlers {
  onNewEmail?: (email: EmailPayload) => void;
  onEmailRead?: (data: EmailStatusPayload) => void;
  onEmailDeleted?: (data: EmailStatusPayload) => void;
  onEmailSent?: (data: any) => void;
  onUnreadCountUpdated?: (data: UnreadCountPayload) => void;
  onMailboxUpdated?: (data: any) => void;
}

export const useEmailEvents = (socket: Socket | null, handlers: EmailEventHandlers) => {
  useEffect(() => {
    if (!socket) return;

    // Register event handlers
    if (handlers.onNewEmail) {
      socket.on("new_email", handlers.onNewEmail);
    }

    if (handlers.onEmailRead) {
      socket.on("email_read", handlers.onEmailRead);
    }

    if (handlers.onEmailDeleted) {
      socket.on("email_deleted", handlers.onEmailDeleted);
    }

    if (handlers.onEmailSent) {
      socket.on("email_sent", handlers.onEmailSent);
    }

    if (handlers.onUnreadCountUpdated) {
      socket.on("unread_count_updated", handlers.onUnreadCountUpdated);
    }

    if (handlers.onMailboxUpdated) {
      socket.on("mailbox_updated", handlers.onMailboxUpdated);
    }

    // Cleanup on unmount
    return () => {
      socket.off("new_email");
      socket.off("email_read");
      socket.off("email_deleted");
      socket.off("email_sent");
      socket.off("unread_count_updated");
      socket.off("mailbox_updated");
    };
  }, [socket, handlers]);
};

📱 Complete React Integration

1. Email Notification Component

// components/EmailNotifications.tsx
import React, { useState, useCallback } from 'react';
import { useEmailWebSocket, useEmailEvents, EmailPayload, UnreadCountPayload } from '../hooks';

interface EmailNotificationsProps {
  userToken: string;
  onEmailClick?: (email: EmailPayload) => void;
}

export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
  userToken,
  onEmailClick
}) => {
  const [notifications, setNotifications] = useState<EmailPayload[]>([]);
  const [unreadCount, setUnreadCount] = useState(0);
  const [isVisible, setIsVisible] = useState(false);

  const { socket, isConnected } = useEmailWebSocket({
    token: userToken,
    onConnect: () => console.log('Email notifications connected'),
    onError: (error) => console.error('WebSocket error:', error),
  });

  const handleNewEmail = useCallback((email: EmailPayload) => {
    console.log('📧 New email received:', email);

    // Add to notifications list
    setNotifications(prev => [email, ...prev.slice(0, 9)]); // Keep only 10 latest

    // Show browser notification if permission granted
    if (Notification.permission === 'granted') {
      new Notification(`📧 New Email: ${email.subject}`, {
        body: `From: ${email.from.address}\n${email.preview}`,
        icon: '/email-icon.png',
        tag: `email-${email.messageId}`, // Prevent duplicates
      });
    }

    // Play notification sound (optional)
    const audio = new Audio('/notification.mp3');
    audio.play().catch(() => {}); // Ignore if audio fails
  }, []);

  const handleUnreadCountUpdate = useCallback((data: UnreadCountPayload) => {
    setUnreadCount(data.count);
  }, []);

  const handleEmailRead = useCallback((data: any) => {
    // Remove from notifications if it was there
    setNotifications(prev =>
      prev.filter(notification => notification.messageId !== data.messageId)
    );
  }, []);

  useEmailEvents(socket, {
    onNewEmail: handleNewEmail,
    onUnreadCountUpdated: handleUnreadCountUpdate,
    onEmailRead: handleEmailRead,
  });

  // Request notification permission on mount
  React.useEffect(() => {
    if (Notification.permission === 'default') {
      Notification.requestPermission();
    }
  }, []);

  return (
    <div className="email-notifications">
      {/* Connection Status */}
      <div className={`connection-status ${isConnected ? 'connected' : 'disconnected'}`}>
        <span className="status-dot"></span>
        {isConnected ? 'Connected' : 'Disconnected'}
      </div>

      {/* Notification Bell */}
      <div
        className="notification-bell"
        onClick={() => setIsVisible(!isVisible)}
      >
        🔔
        {unreadCount > 0 && (
          <span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
        )}
      </div>

      {/* Notifications Dropdown */}
      {isVisible && (
        <div className="notifications-dropdown">
          <div className="notifications-header">
            <h3>📧 Email Notifications</h3>
            <span>{unreadCount} unread</span>
          </div>

          <div className="notifications-list">
            {notifications.length === 0 ? (
              <div className="no-notifications">No new emails</div>
            ) : (
              notifications.map((email) => (
                <div
                  key={email.messageId}
                  className="notification-item"
                  onClick={() => onEmailClick?.(email)}
                >
                  <div className="sender">
                    <strong>{email.from.name || email.from.address}</strong>
                    {email.hasAttachments && <span className="attachment-icon">📎</span>}
                  </div>
                  <div className="subject">{email.subject}</div>
                  <div className="preview">{email.preview}</div>
                  <div className="timestamp">
                    {new Date(email.timestamp).toLocaleString()}
                  </div>
                </div>
              ))
            )}
          </div>
        </div>
      )}
    </div>
  );
};

2. Email Dashboard Component

// components/EmailDashboard.tsx
import React, { useState } from 'react';
import { useEmailWebSocket, useEmailEvents } from '../hooks';

interface EmailDashboardProps {
  userToken: string;
}

export const EmailDashboard: React.FC<EmailDashboardProps> = ({ userToken }) => {
  const [emailStats, setEmailStats] = useState({
    totalUnread: 0,
    newToday: 0,
    sentToday: 0,
  });

  const { socket, isConnected } = useEmailWebSocket({
    token: userToken,
  });

  useEmailEvents(socket, {
    onNewEmail: (email) => {
      setEmailStats(prev => ({
        ...prev,
        totalUnread: prev.totalUnread + 1,
        newToday: prev.newToday + 1,
      }));
    },

    onEmailSent: () => {
      setEmailStats(prev => ({
        ...prev,
        sentToday: prev.sentToday + 1,
      }));
    },

    onUnreadCountUpdated: (data) => {
      setEmailStats(prev => ({
        ...prev,
        totalUnread: data.count,
      }));
    },
  });

  return (
    <div className="email-dashboard">
      <h2>📧 Email Dashboard</h2>

      <div className="stats-grid">
        <div className="stat-card">
          <h3>{emailStats.totalUnread}</h3>
          <p>Unread Emails</p>
        </div>

        <div className="stat-card">
          <h3>{emailStats.newToday}</h3>
          <p>Received Today</p>
        </div>

        <div className="stat-card">
          <h3>{emailStats.sentToday}</h3>
          <p>Sent Today</p>
        </div>

        <div className="stat-card">
          <h3 className={isConnected ? 'connected' : 'disconnected'}>
            {isConnected ? '🟢' : '🔴'}
          </h3>
          <p>{isConnected ? 'Connected' : 'Disconnected'}</p>
        </div>
      </div>
    </div>
  );
};

3. Context Provider (Advanced)

// context/EmailWebSocketContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { useEmailWebSocket } from '../hooks/useEmailWebSocket';
import { Socket } from 'socket.io-client';

interface EmailWebSocketContextValue {
  socket: Socket | null;
  isConnected: boolean;
}

const EmailWebSocketContext = createContext<EmailWebSocketContextValue | null>(null);

interface EmailWebSocketProviderProps {
  children: ReactNode;
  token: string;
  serverUrl?: string;
}

export const EmailWebSocketProvider: React.FC<EmailWebSocketProviderProps> = ({
  children,
  token,
  serverUrl,
}) => {
  const { socket, isConnected } = useEmailWebSocket({ token, serverUrl });

  return (
    <EmailWebSocketContext.Provider value={{ socket, isConnected }}>
      {children}
    </EmailWebSocketContext.Provider>
  );
};

export const useEmailWebSocketContext = () => {
  const context = useContext(EmailWebSocketContext);
  if (!context) {
    throw new Error('useEmailWebSocketContext must be used within EmailWebSocketProvider');
  }
  return context;
};

🎨 CSS Styles

/* styles/email-notifications.css */
.email-notifications {
  position: relative;
  display: inline-block;
}

.connection-status {
  position: absolute;
  top: -8px;
  right: -8px;
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  gap: 4px;
}

.connection-status.connected {
  background: #d4edda;
  color: #155724;
}

.connection-status.disconnected {
  background: #f8d7da;
  color: #721c24;
}

.status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.notification-bell {
  position: relative;
  cursor: pointer;
  font-size: 24px;
  padding: 8px;
  border-radius: 50%;
  transition: background-color 0.2s;
}

.notification-bell:hover {
  background-color: #f5f5f5;
}

.badge {
  position: absolute;
  top: 0;
  right: 0;
  background: #dc3545;
  color: white;
  border-radius: 50%;
  min-width: 18px;
  height: 18px;
  font-size: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}

.notifications-dropdown {
  position: absolute;
  top: 100%;
  right: 0;
  width: 350px;
  max-height: 400px;
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  z-index: 1000;
  overflow: hidden;
}

.notifications-header {
  padding: 12px 16px;
  background: #f8f9fa;
  border-bottom: 1px solid #dee2e6;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.notifications-list {
  max-height: 300px;
  overflow-y: auto;
}

.notification-item {
  padding: 12px 16px;
  border-bottom: 1px solid #eee;
  cursor: pointer;
  transition: background-color 0.2s;
}

.notification-item:hover {
  background-color: #f8f9fa;
}

.notification-item:last-child {
  border-bottom: none;
}

.sender {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}

.attachment-icon {
  font-size: 12px;
}

.subject {
  font-weight: 500;
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.preview {
  color: #666;
  font-size: 14px;
  margin-bottom: 4px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.timestamp {
  font-size: 12px;
  color: #999;
}

.no-notifications {
  padding: 20px;
  text-align: center;
  color: #666;
}

/* Dashboard Styles */
.email-dashboard {
  padding: 20px;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
  margin-top: 20px;
}

.stat-card {
  background: white;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #ddd;
  text-align: center;
}

.stat-card h3 {
  margin: 0 0 8px 0;
  font-size: 32px;
  font-weight: bold;
}

.stat-card h3.connected {
  color: #28a745;
}

.stat-card h3.disconnected {
  color: #dc3545;
}

.stat-card p {
  margin: 0;
  color: #666;
  font-size: 14px;
}

🧪 Testing & Manual Triggers

Manual Email Check

// API call to manually trigger email check
const triggerEmailCheck = async () => {
  try {
    const response = await fetch("/api/email/gateway/check-emails", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${userToken}`,
        "Content-Type": "application/json",
      },
    });

    const result = await response.json();
    console.log("✅ Manual email check triggered:", result);
  } catch (error) {
    console.error("❌ Failed to trigger email check:", error);
  }
};

Send Test Notification

const sendTestNotification = async () => {
  try {
    const response = await fetch("/api/email/gateway/test-notification", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${userToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        message: "Test notification from React app!",
      }),
    });

    const result = await response.json();
    console.log("✅ Test notification sent:", result);
  } catch (error) {
    console.error("❌ Failed to send test notification:", error);
  }
};

🔧 Complete App Integration

// App.tsx - Complete example
import React from 'react';
import { EmailWebSocketProvider } from './context/EmailWebSocketContext';
import { EmailNotifications } from './components/EmailNotifications';
import { EmailDashboard } from './components/EmailDashboard';

function App() {
  const userToken = localStorage.getItem('jwt-token') || '';

  const handleEmailClick = (email: any) => {
    console.log('Email clicked:', email);
    // Navigate to email detail page
    // router.push(`/emails/${email.messageId}`);
  };

  if (!userToken) {
    return <div>Please login to access email notifications</div>;
  }

  return (
    <EmailWebSocketProvider token={userToken}>
      <div className="app">
        <header>
          <h1>📧 Email App</h1>
          <EmailNotifications
            userToken={userToken}
            onEmailClick={handleEmailClick}
          />
        </header>

        <main>
          <EmailDashboard userToken={userToken} />
        </main>
      </div>
    </EmailWebSocketProvider>
  );
}

export default App;

🚨 Error Handling & Edge Cases

Connection Recovery

const useConnectionRecovery = (socket: Socket | null) => {
  useEffect(() => {
    if (!socket) return;

    const handleReconnect = () => {
      console.log("🔄 WebSocket reconnecting...");
      // Optionally refresh data or show reconnecting state
    };

    const handleReconnectError = () => {
      console.error("❌ Failed to reconnect");
      // Show user notification or retry logic
    };

    socket.on("reconnect", handleReconnect);
    socket.on("reconnect_error", handleReconnectError);

    return () => {
      socket.off("reconnect", handleReconnect);
      socket.off("reconnect_error", handleReconnectError);
    };
  }, [socket]);
};

Token Refresh

const useTokenRefresh = (socket: Socket | null) => {
  useEffect(() => {
    const refreshToken = () => {
      const newToken = localStorage.getItem("jwt-token");
      if (socket && newToken) {
        socket.auth = { token: newToken };
        socket.disconnect();
        socket.connect();
      }
    };

    // Listen for token refresh events
    window.addEventListener("token-refreshed", refreshToken);

    return () => {
      window.removeEventListener("token-refreshed", refreshToken);
    };
  }, [socket]);
};

📝 Environment Configuration

// config/websocket.ts
export const WEBSOCKET_CONFIG = {
  development: {
    serverUrl: "ws://localhost:3000/email",
    reconnectionAttempts: 5,
    timeout: 20000,
  },
  production: {
    serverUrl: "wss://your-domain.com/email",
    reconnectionAttempts: 10,
    timeout: 30000,
  },
};

export const getWebSocketConfig = () => {
  const env = process.env.NODE_ENV || "development";
  return WEBSOCKET_CONFIG[env as keyof typeof WEBSOCKET_CONFIG];
};

🎯 Best Practices

  1. 🔐 Security: Always use HTTPS/WSS in production
  2. 🔄 Reconnection: Implement proper reconnection logic
  3. 🎯 Performance: Limit notification history (max 10-50 items)
  4. 📱 UX: Request notification permissions gracefully
  5. 🔊 Audio: Make notification sounds optional
  6. 💾 Persistence: Consider storing notifications in localStorage
  7. 🎨 Theming: Support light/dark mode for notifications
  8. Accessibility: Add proper ARIA labels and keyboard navigation

🐛 Troubleshooting

Connection Issues:

  • Check JWT token validity
  • Verify server URL and port
  • Check browser console for errors
  • Ensure CORS is properly configured

Missing Notifications:

  • Verify WebSocket event names match backend
  • Check browser notification permissions
  • Confirm user is authenticated properly
  • Test with manual triggers

Performance Issues:

  • Limit notification history
  • Debounce rapid events
  • Use React.memo for components
  • Implement virtual scrolling for long lists

🚀 Ready to Go!

Your React app is now ready to receive real-time email notifications! The WebSocket connection will automatically:

  • Authenticate users with JWT tokens
  • Receive new email notifications instantly
  • Update unread counts in real-time
  • Handle connection recovery automatically
  • Show browser notifications (with permission)
  • Provide a beautiful UI for email notifications

Happy coding! 🎉