From 61e3be9f4a5fd338e628c9e2106eb68a14afa41e Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 26 Jul 2025 10:11:17 +0330 Subject: [PATCH] feat: change the email structure into seprate module --- REACT_WEB_PUSH_README.md | 1153 +++++++++++++++++ src/app.module.ts | 4 + .../email-gateway/email-gateway.module.ts | 13 + .../email.gateway.ts | 14 +- .../services/websocket-auth.service.ts | 4 +- src/modules/email-utils/email-utils.module.ts | 25 + .../queue/email-monitoring.processor.ts | 6 +- .../services/email-monitoring.service.ts | 7 +- .../services/email-notification.service.ts | 12 +- .../services/mailbox-resolver.service.ts | 2 +- src/modules/email/email.module.ts | 46 +- src/modules/email/services/email.service.ts | 4 +- .../interfaces/ISendNotificationData.ts | 1 + .../services/notifications.service.ts | 2 +- src/modules/users/services/users.service.ts | 4 - 15 files changed, 1226 insertions(+), 71 deletions(-) create mode 100644 REACT_WEB_PUSH_README.md create mode 100644 src/modules/email-gateway/email-gateway.module.ts rename src/modules/{email/gateways => email-gateway}/email.gateway.ts (95%) rename src/modules/{email => email-gateway}/services/websocket-auth.service.ts (97%) create mode 100644 src/modules/email-utils/email-utils.module.ts rename src/modules/{email => email-utils}/queue/email-monitoring.processor.ts (83%) rename src/modules/{email => email-utils}/services/email-monitoring.service.ts (98%) rename src/modules/{email => email-utils}/services/email-notification.service.ts (98%) rename src/modules/{email => email-utils}/services/mailbox-resolver.service.ts (98%) diff --git a/REACT_WEB_PUSH_README.md b/REACT_WEB_PUSH_README.md new file mode 100644 index 0000000..0e0ea1f --- /dev/null +++ b/REACT_WEB_PUSH_README.md @@ -0,0 +1,1153 @@ +# React Web Push Notifications - Frontend Implementation Guide + +A complete guide for implementing web push notifications in your React application with the Dmail API. + +## ๐Ÿš€ Quick Start + +### Prerequisites + +- React 16.8+ (for hooks support) +- TypeScript (recommended) +- HTTPS-enabled domain (required for service workers) +- Modern browser support (Chrome 50+, Firefox 44+, Safari 16+, Edge 17+) + +### Installation + +```bash +# No additional dependencies needed - uses native Web Push API +# Optional: Install types for better TypeScript support +npm install --save-dev @types/web-push +``` + +## ๐Ÿ“ Project Structure + +``` +src/ +โ”œโ”€โ”€ hooks/ +โ”‚ โ”œโ”€โ”€ usePushNotifications.ts +โ”‚ โ””โ”€โ”€ useServiceWorker.ts +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ PushNotificationPrompt.tsx +โ”‚ โ”œโ”€โ”€ NotificationSettings.tsx +โ”‚ โ””โ”€โ”€ PushNotificationButton.tsx +โ”œโ”€โ”€ contexts/ +โ”‚ โ””โ”€โ”€ PushNotificationContext.tsx +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ pushNotificationService.ts +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ pushNotifications.ts +โ””โ”€โ”€ utils/ + โ””โ”€โ”€ vapidHelper.ts +public/ +โ””โ”€โ”€ sw.js (Service Worker) +``` + +## ๐Ÿ”ง Implementation + +### 1. Service Worker Setup + +Create `public/sw.js`: + +```javascript +// Service Worker for handling push notifications +const CACHE_NAME = "dmail-push-v1"; + +// Install event +self.addEventListener("install", (event) => { + console.log("Service Worker installing"); + self.skipWaiting(); +}); + +// Activate event +self.addEventListener("activate", (event) => { + console.log("Service Worker activating"); + event.waitUntil(clients.claim()); +}); + +// Push event handler +self.addEventListener("push", (event) => { + console.log("Push message received:", event); + + if (!event.data) { + console.log("Push event but no data"); + return; + } + + try { + const data = event.data.json(); + console.log("Push data:", data); + + const options = { + body: data.body, + icon: data.icon || "/icons/email-icon-192.png", + badge: data.badge || "/icons/email-badge-72.png", + tag: data.tag || "email-notification", + requireInteraction: data.requireInteraction || true, + actions: data.actions || [ + { + action: "view", + title: "View Email", + icon: "/icons/view-icon-32.png", + }, + { + action: "dismiss", + title: "Dismiss", + }, + ], + data: { + ...data.data, + timestamp: Date.now(), + }, + vibrate: data.vibrate || [200, 100, 200], + silent: data.silent || false, + }; + + event.waitUntil(self.registration.showNotification(data.title, options)); + } catch (error) { + console.error("Error parsing push data:", error); + + // Show generic notification if parsing fails + event.waitUntil( + self.registration.showNotification("New Email", { + body: "You have received a new email", + icon: "/icons/email-icon-192.png", + tag: "generic-email", + }), + ); + } +}); + +// Notification click handler +self.addEventListener("notificationclick", (event) => { + console.log("Notification clicked:", event); + + event.notification.close(); + + const action = event.action; + const data = event.notification.data; + + if (action === "view") { + // Open the email in the app + event.waitUntil(clients.openWindow(data.url || "/inbox")); + } else if (action === "dismiss") { + // Just close the notification (already done above) + return; + } else { + // Default click action - open the app + event.waitUntil(clients.openWindow(data.url || "/")); + } +}); + +// Background sync (optional - for future enhancements) +self.addEventListener("sync", (event) => { + if (event.tag === "email-sync") { + event.waitUntil( + // Handle background email sync + console.log("Background sync triggered"), + ); + } +}); +``` + +### 2. TypeScript Types + +Create `src/types/pushNotifications.ts`: + +```typescript +export interface WebPushSubscription { + endpoint: string; + keys: { + p256dh: string; + auth: string; + }; +} + +export interface RegisterPushSubscriptionRequest { + subscription: WebPushSubscription; + deviceType?: string; + deviceName?: string; +} + +export interface PushNotificationSettings { + enabled: boolean; + subscriptionCount: number; + lastPushNotificationAt?: string; + serviceEnabled: boolean; + vapidPublicKey: string; +} + +export interface TestNotificationRequest { + message?: string; +} + +export interface PushNotificationInfo { + pushNotificationsEnabled: boolean; + subscriptionCount: number; + lastPushNotificationAt?: string; + serviceEnabled: boolean; + vapidPublicKey: string; + serviceInfo: { + enabled: boolean; + configured: boolean; + vapidPublicKey: string; + ttl: number; + urgency: string; + }; + setup: { + instructions: string; + documentation: { + webPushAPI: string; + serviceWorkers: string; + }; + }; +} + +export interface PushNotificationContextValue { + isSupported: boolean; + isSubscribed: boolean; + isLoading: boolean; + error: string | null; + subscriptionCount: number; + subscribe: () => Promise; + unsubscribe: () => Promise; + testNotification: () => Promise; + updateSettings: (enabled: boolean) => Promise; + clearError: () => void; +} +``` + +### 3. API Service + +Create `src/services/pushNotificationService.ts`: + +```typescript +import { WebPushSubscription, RegisterPushSubscriptionRequest, PushNotificationInfo, TestNotificationRequest } from "../types/pushNotifications"; + +class PushNotificationService { + private baseURL: string; + private getAuthToken: () => string | null; + + constructor(baseURL: string, getAuthToken: () => string | null) { + this.baseURL = baseURL; + this.getAuthToken = getAuthToken; + } + + private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { + const token = this.getAuthToken(); + + const response = await fetch(`${this.baseURL}${endpoint}`, { + ...options, + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + ...options.headers, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + } + + async getVAPIDPublicKey(): Promise<{ vapidPublicKey: string }> { + return this.makeRequest("/users/push-notifications/vapid-key"); + } + + async registerSubscription(data: RegisterPushSubscriptionRequest): Promise { + return this.makeRequest("/users/push-subscriptions", { + method: "POST", + body: JSON.stringify(data), + }); + } + + async removeSubscription(endpoint: string): Promise { + return this.makeRequest("/users/push-subscriptions", { + method: "DELETE", + body: JSON.stringify({ endpoint }), + }); + } + + async updateSettings(enabled: boolean): Promise { + return this.makeRequest("/users/push-notifications/settings", { + method: "PATCH", + body: JSON.stringify({ enabled }), + }); + } + + async getNotificationInfo(): Promise { + return this.makeRequest("/users/push-notifications/info"); + } + + async testNotification(data: TestNotificationRequest = {}): Promise { + return this.makeRequest("/users/push-notifications/test", { + method: "POST", + body: JSON.stringify(data), + }); + } +} + +export default PushNotificationService; +``` + +### 4. Utility Helper + +Create `src/utils/vapidHelper.ts`: + +```typescript +export function urlBase64ToUint8Array(base64String: string): Uint8Array { + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} + +export function getBrowserInfo(): string { + const userAgent = navigator.userAgent; + let browserName = "Unknown"; + + if (userAgent.includes("Chrome")) { + browserName = "Chrome"; + } else if (userAgent.includes("Firefox")) { + browserName = "Firefox"; + } else if (userAgent.includes("Safari")) { + browserName = "Safari"; + } else if (userAgent.includes("Edge")) { + browserName = "Edge"; + } else if (userAgent.includes("Opera")) { + browserName = "Opera"; + } + + return `${browserName} on ${navigator.platform}`; +} + +export function isPushSupported(): boolean { + return "serviceWorker" in navigator && "PushManager" in window && "Notification" in window; +} +``` + +### 5. Service Worker Hook + +Create `src/hooks/useServiceWorker.ts`: + +```typescript +import { useEffect, useState, useCallback } from "react"; + +interface UseServiceWorkerReturn { + registration: ServiceWorkerRegistration | null; + isLoading: boolean; + error: string | null; + register: () => Promise; +} + +export function useServiceWorker(scriptURL = "/sw.js"): UseServiceWorkerReturn { + const [registration, setRegistration] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const register = useCallback(async () => { + if (!("serviceWorker" in navigator)) { + setError("Service Workers are not supported in this browser"); + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + setError(null); + + const reg = await navigator.serviceWorker.register(scriptURL); + + // Wait for the service worker to be ready + await navigator.serviceWorker.ready; + + setRegistration(reg); + console.log("Service Worker registered successfully:", reg); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to register service worker"; + setError(errorMessage); + console.error("Service Worker registration failed:", err); + } finally { + setIsLoading(false); + } + }, [scriptURL]); + + useEffect(() => { + register(); + }, [register]); + + return { registration, isLoading, error, register }; +} +``` + +### 6. Push Notifications Hook + +Create `src/hooks/usePushNotifications.ts`: + +```typescript +import { useState, useEffect, useCallback } from "react"; +import { WebPushSubscription } from "../types/pushNotifications"; +import PushNotificationService from "../services/pushNotificationService"; +import { urlBase64ToUint8Array, getBrowserInfo, isPushSupported } from "../utils/vapidHelper"; + +interface UsePushNotificationsProps { + apiService: PushNotificationService; +} + +interface UsePushNotificationsReturn { + isSupported: boolean; + isSubscribed: boolean; + isLoading: boolean; + error: string | null; + subscriptionCount: number; + permission: NotificationPermission; + subscribe: () => Promise; + unsubscribe: () => Promise; + testNotification: () => Promise; + checkSubscriptionStatus: () => Promise; + clearError: () => void; +} + +export function usePushNotifications({ apiService }: UsePushNotificationsProps): UsePushNotificationsReturn { + const [isSubscribed, setIsSubscribed] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [subscriptionCount, setSubscriptionCount] = useState(0); + const [permission, setPermission] = useState("default"); + + const isSupported = isPushSupported(); + + const clearError = useCallback(() => { + setError(null); + }, []); + + const checkSubscriptionStatus = useCallback(async () => { + if (!isSupported) return; + + try { + setIsLoading(true); + + // Check browser subscription + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + setIsSubscribed(!!subscription); + + // Check server-side info + const info = await apiService.getNotificationInfo(); + setSubscriptionCount(info.subscriptionCount); + + // Check permission status + setPermission(Notification.permission); + } catch (err) { + console.error("Error checking subscription status:", err); + setError(err instanceof Error ? err.message : "Failed to check subscription status"); + } finally { + setIsLoading(false); + } + }, [apiService, isSupported]); + + const subscribe = useCallback(async (): Promise => { + if (!isSupported) { + throw new Error("Push notifications are not supported in this browser"); + } + + try { + setIsLoading(true); + setError(null); + + // Request notification permission + const permission = await Notification.requestPermission(); + setPermission(permission); + + if (permission !== "granted") { + throw new Error("Notification permission denied"); + } + + // Get VAPID public key + const { vapidPublicKey } = await apiService.getVAPIDPublicKey(); + + if (!vapidPublicKey) { + throw new Error("VAPID public key not available"); + } + + // Get service worker registration + const registration = await navigator.serviceWorker.ready; + + // Create push subscription + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), + }); + + // Register subscription with server + await apiService.registerSubscription({ + subscription: subscription.toJSON() as WebPushSubscription, + deviceType: "web", + deviceName: getBrowserInfo(), + }); + + setIsSubscribed(true); + await checkSubscriptionStatus(); // Refresh subscription count + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to subscribe to push notifications"; + setError(errorMessage); + throw new Error(errorMessage); + } finally { + setIsLoading(false); + } + }, [apiService, isSupported, checkSubscriptionStatus]); + + const unsubscribe = useCallback(async (): Promise => { + if (!isSupported) return; + + try { + setIsLoading(true); + setError(null); + + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + + if (subscription) { + // Unsubscribe from browser + await subscription.unsubscribe(); + + // Remove from server + await apiService.removeSubscription(subscription.endpoint); + } + + setIsSubscribed(false); + await checkSubscriptionStatus(); // Refresh subscription count + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to unsubscribe from push notifications"; + setError(errorMessage); + throw new Error(errorMessage); + } finally { + setIsLoading(false); + } + }, [apiService, isSupported, checkSubscriptionStatus]); + + const testNotification = useCallback(async (): Promise => { + try { + setError(null); + await apiService.testNotification({ + message: "Test notification from your React app!", + }); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to send test notification"; + setError(errorMessage); + throw new Error(errorMessage); + } + }, [apiService]); + + useEffect(() => { + if (isSupported) { + checkSubscriptionStatus(); + } else { + setIsLoading(false); + } + }, [checkSubscriptionStatus, isSupported]); + + return { + isSupported, + isSubscribed, + isLoading, + error, + subscriptionCount, + permission, + subscribe, + unsubscribe, + testNotification, + checkSubscriptionStatus, + clearError, + }; +} +``` + +### 7. React Context Provider + +Create `src/contexts/PushNotificationContext.tsx`: + +```tsx +import React, { createContext, useContext, ReactNode } from "react"; +import { usePushNotifications } from "../hooks/usePushNotifications"; +import PushNotificationService from "../services/pushNotificationService"; +import { PushNotificationContextValue } from "../types/pushNotifications"; + +const PushNotificationContext = createContext(undefined); + +interface PushNotificationProviderProps { + children: ReactNode; + apiBaseURL: string; + getAuthToken: () => string | null; +} + +export function PushNotificationProvider({ children, apiBaseURL, getAuthToken }: PushNotificationProviderProps) { + const apiService = new PushNotificationService(apiBaseURL, getAuthToken); + + const { isSupported, isSubscribed, isLoading, error, subscriptionCount, subscribe, unsubscribe, testNotification, clearError } = + usePushNotifications({ apiService }); + + const updateSettings = async (enabled: boolean): Promise => { + await apiService.updateSettings(enabled); + }; + + const value: PushNotificationContextValue = { + isSupported, + isSubscribed, + isLoading, + error, + subscriptionCount, + subscribe, + unsubscribe, + testNotification, + updateSettings, + clearError, + }; + + return {children}; +} + +export function usePushNotificationContext(): PushNotificationContextValue { + const context = useContext(PushNotificationContext); + if (context === undefined) { + throw new Error("usePushNotificationContext must be used within a PushNotificationProvider"); + } + return context; +} +``` + +### 8. React Components + +#### Push Notification Button Component + +Create `src/components/PushNotificationButton.tsx`: + +```tsx +import React from "react"; +import { usePushNotificationContext } from "../contexts/PushNotificationContext"; + +interface PushNotificationButtonProps { + className?: string; + enabledText?: string; + disabledText?: string; + loadingText?: string; + unsupportedText?: string; +} + +export function PushNotificationButton({ + className = "btn btn-primary", + enabledText = "Disable Notifications", + disabledText = "Enable Notifications", + loadingText = "Loading...", + unsupportedText = "Not Supported", +}: PushNotificationButtonProps) { + const { isSupported, isSubscribed, isLoading, error, subscribe, unsubscribe, clearError } = usePushNotificationContext(); + + const handleClick = async () => { + try { + clearError(); + if (isSubscribed) { + await unsubscribe(); + } else { + await subscribe(); + } + } catch (err) { + // Error is handled by the context + console.error("Push notification action failed:", err); + } + }; + + if (!isSupported) { + return ( + + ); + } + + return ( +
+ + + {error &&
{error}
} +
+ ); +} +``` + +#### Notification Settings Component + +Create `src/components/NotificationSettings.tsx`: + +```tsx +import React from "react"; +import { usePushNotificationContext } from "../contexts/PushNotificationContext"; + +export function NotificationSettings() { + const { isSupported, isSubscribed, isLoading, error, subscriptionCount, subscribe, unsubscribe, testNotification, clearError } = + usePushNotificationContext(); + + const handleTest = async () => { + try { + clearError(); + await testNotification(); + alert("Test notification sent! Check your browser notifications."); + } catch (err) { + console.error("Test notification failed:", err); + } + }; + + if (!isSupported) { + return ( +
+

Push Notifications Not Supported

+

+ Your browser doesn't support push notifications. Please use a modern browser like Chrome, Firefox, Safari 16+, or Edge. +

+
+ ); + } + + return ( +
+

๐Ÿ“ฌ Email Notifications

+ +
+
+
+

Desktop Notifications

+

Get notified instantly when new emails arrive, even when the app is closed.

+
+ +
+ +
+
+ + {error && ( +
+

{error}

+ +
+ )} + + {isSubscribed && ( +
+
+

+ โœ… Notifications are enabled! You have {subscriptionCount} active subscription{subscriptionCount !== 1 ? "s" : ""}. +

+
+ + +
+ )} + +
+

+ ๐Ÿ”’ Your notification preferences are stored securely and can be changed at any time. Notifications work even when your browser is closed. +

+
+
+
+ ); +} +``` + +#### Notification Prompt Component + +Create `src/components/PushNotificationPrompt.tsx`: + +```tsx +import React, { useState } from "react"; +import { usePushNotificationContext } from "../contexts/PushNotificationContext"; + +interface PushNotificationPromptProps { + onDismiss?: () => void; + autoShow?: boolean; +} + +export function PushNotificationPrompt({ onDismiss, autoShow = true }: PushNotificationPromptProps) { + const [isDismissed, setIsDismissed] = useState(false); + const { isSupported, isSubscribed, isLoading, subscribe, clearError } = usePushNotificationContext(); + + const handleEnable = async () => { + try { + clearError(); + await subscribe(); + } catch (err) { + console.error("Failed to enable notifications:", err); + } + }; + + const handleDismiss = () => { + setIsDismissed(true); + onDismiss?.(); + }; + + // Don't show if not supported, already subscribed, or manually dismissed + if (!isSupported || isSubscribed || isDismissed || !autoShow) { + return null; + } + + return ( +
+
+
+
๐Ÿ“ง
+
+

Stay Updated with Email Notifications

+

Get instant notifications when new emails arrive, even when the app is closed.

+
+ +
+ +
+ + +
+
+
+ ); +} +``` + +### 9. App Integration + +Update your main `App.tsx`: + +```tsx +import React from "react"; +import { PushNotificationProvider } from "./contexts/PushNotificationContext"; +import { NotificationSettings } from "./components/NotificationSettings"; +import { PushNotificationPrompt } from "./components/PushNotificationPrompt"; + +function App() { + // Replace with your actual API base URL and auth token logic + const apiBaseURL = process.env.REACT_APP_API_URL || "https://api.yourdomain.com"; + const getAuthToken = () => localStorage.getItem("authToken"); + + return ( + +
+ {/* Your existing app content */} +
+

Dmail - Email Client

+
+ +
+ {/* Your existing routes and components */} + + {/* Add notification settings to your settings page */} +
+ +
+
+ + {/* Notification prompt - shows automatically for new users */} + +
+
+ ); +} + +export default App; +``` + +## ๐ŸŽจ Styling Examples + +### Tailwind CSS Classes + +The components use Tailwind CSS classes. If you're not using Tailwind, here are equivalent CSS styles: + +```css +/* Button styles */ +.btn { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + border: none; + transition: background-color 0.2s; +} + +.btn-primary { + background-color: #3b82f6; + color: white; +} + +.btn-primary:hover { + background-color: #2563eb; +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Notification prompt */ +.notification-prompt { + position: fixed; + bottom: 1rem; + right: 1rem; + max-width: 24rem; + background: white; + border: 1px solid #d1d5db; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + z-index: 50; + padding: 1rem; +} + +/* Error messages */ +.error-message { + background-color: #fef2f2; + border: 1px solid #fecaca; + color: #dc2626; + padding: 0.75rem; + border-radius: 0.375rem; + font-size: 0.875rem; +} + +/* Success messages */ +.success-message { + background-color: #f0fdf4; + border: 1px solid #bbf7d0; + color: #16a34a; + padding: 0.75rem; + border-radius: 0.375rem; + font-size: 0.875rem; +} +``` + +## ๐Ÿงช Testing + +### Testing Push Notifications + +1. **Development Testing**: + + ```typescript + // Add to your component for testing + const { testNotification } = usePushNotificationContext(); + + const handleTest = async () => { + try { + await testNotification(); + console.log("Test notification sent!"); + } catch (error) { + console.error("Test failed:", error); + } + }; + ``` + +2. **Browser Developer Tools**: + - Open Chrome DevTools โ†’ Application โ†’ Service Workers + - Check if your service worker is registered and active + - Use Application โ†’ Storage โ†’ IndexedDB to inspect subscription data + +3. **Manual Testing Checklist**: + - [ ] Service worker registers successfully + - [ ] Permission request works + - [ ] Subscription creates and stores properly + - [ ] Test notification displays correctly + - [ ] Notification actions work (click to open app) + - [ ] Unsubscribe removes subscription + - [ ] Works when browser is closed + +## ๐Ÿ“ฑ Icons and Assets + +Add these icons to your `public/icons/` directory: + +- `email-icon-192.png` - Main notification icon (192x192px) +- `email-badge-72.png` - Notification badge (72x72px) +- `view-icon-32.png` - Action button icon (32x32px) + +## ๐Ÿ”ง Environment Variables + +Create a `.env` file in your React app: + +```bash +# API Configuration +REACT_APP_API_URL=https://api.yourdomain.com + +# Optional: Enable debug mode +REACT_APP_DEBUG_PUSH=true + +# Optional: Custom service worker path +REACT_APP_SW_PATH=/sw.js +``` + +## ๐Ÿš€ Deployment Considerations + +### HTTPS Requirement + +- Push notifications require HTTPS in production +- Service workers only work over HTTPS (except on localhost) +- Ensure your domain has a valid SSL certificate + +### Service Worker Caching + +- The service worker file (`sw.js`) should not be cached by your CDN +- Add appropriate cache headers: + ``` + Cache-Control: no-cache, no-store, must-revalidate + ``` + +### Build Configuration + +- Ensure `sw.js` is copied to your build output +- Most React build tools automatically copy files from `public/` + +## ๐Ÿ› Troubleshooting + +### Common Issues + +1. **Service Worker Not Registering**: + + ```javascript + // Check in browser console + navigator.serviceWorker.getRegistrations().then((registrations) => { + console.log("Active registrations:", registrations); + }); + ``` + +2. **Push Subscription Fails**: + - Verify VAPID keys are correct + - Check browser permissions + - Ensure HTTPS is used + +3. **Notifications Not Appearing**: + - Check browser notification settings + - Verify notification permission is granted + - Check if "Do Not Disturb" is enabled + +4. **CORS Issues**: + - Ensure your API allows requests from your frontend domain + - Check CORS headers include your domain + +### Debug Mode + +Enable debug logging: + +```typescript +// Add to your service or component +const DEBUG = process.env.REACT_APP_DEBUG_PUSH === "true"; + +if (DEBUG) { + console.log("Push notification debug info:", { + isSupported: isPushSupported(), + permission: Notification.permission, + serviceWorkerReady: "serviceWorker" in navigator, + }); +} +``` + +## ๐Ÿ”’ Security Best Practices + +1. **Validate Permissions**: Always check notification permission before attempting to subscribe +2. **Error Handling**: Implement proper error boundaries and user feedback +3. **Token Management**: Securely store and refresh authentication tokens +4. **Content Security Policy**: Configure CSP headers to allow service worker execution +5. **VAPID Keys**: Never expose private VAPID keys in frontend code + +## ๐Ÿ“ˆ Analytics and Monitoring + +Track push notification metrics: + +```typescript +// Example analytics tracking +const trackPushEvent = (event: string, data?: any) => { + // Replace with your analytics service + if (window.gtag) { + window.gtag("event", event, { + event_category: "push_notifications", + ...data, + }); + } +}; + +// Usage in components +const { subscribe } = usePushNotificationContext(); + +const handleSubscribe = async () => { + try { + await subscribe(); + trackPushEvent("push_subscription_success"); + } catch (error) { + trackPushEvent("push_subscription_error", { error: error.message }); + } +}; +``` + +## ๐Ÿš€ Performance Optimization + +1. **Lazy Loading**: Load push notification components only when needed +2. **Service Worker Caching**: Cache API responses for better offline experience +3. **Bundle Splitting**: Separate push notification code into its own chunk +4. **Error Boundaries**: Prevent push notification errors from crashing the app + +## ๐Ÿ“‹ Checklist for Production + +- [ ] HTTPS enabled on production domain +- [ ] VAPID keys generated and configured +- [ ] Service worker file accessible at `/sw.js` +- [ ] Icons and assets optimized and available +- [ ] Error handling implemented +- [ ] Browser compatibility tested +- [ ] Permission request UX implemented +- [ ] Analytics tracking configured +- [ ] Backup notification strategy (email fallback) + +## ๐Ÿ†˜ Support and Resources + +- [Web Push API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) +- [Service Workers Guide](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) +- [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) +- [VAPID Specification](https://tools.ietf.org/html/rfc8292) + +--- + +## ๐ŸŽ‰ You're Ready! + +This complete React implementation provides: + +- โœ… Type-safe TypeScript integration +- โœ… Modern React hooks and context +- โœ… Comprehensive error handling +- โœ… Production-ready components +- โœ… Excellent user experience +- โœ… Full browser compatibility + +Your users will now receive instant push notifications for new emails, even when your web app is closed! ๐Ÿš€ diff --git a/src/app.module.ts b/src/app.module.ts index cf1e131..fa4dcff 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,6 +18,8 @@ import { AiAssistantModule } from "./modules/ai-assistant/ai-assistant.module"; import { AuthModule } from "./modules/auth/auth.module"; import { DomainsModule } from "./modules/domains/domains.module"; import { EmailModule } from "./modules/email/email.module"; +import { EmailGatewayModule } from "./modules/email-gateway/email-gateway.module"; +import { EmailUtilsModule } from "./modules/email-utils/email-utils.module"; import { MailServerModule } from "./modules/mail-server/mail-server.module"; import { MailboxModule } from "./modules/mailbox/mailbox.module"; import { NotificationModule } from "./modules/notifications/notifications.module"; @@ -52,6 +54,8 @@ import { UtilsModule } from "./modules/utils/utils.module"; SettingModule, NotificationModule, UploaderModule, + EmailUtilsModule, + EmailGatewayModule, ], }) export class AppModule implements NestModule { diff --git a/src/modules/email-gateway/email-gateway.module.ts b/src/modules/email-gateway/email-gateway.module.ts new file mode 100644 index 0000000..df67b34 --- /dev/null +++ b/src/modules/email-gateway/email-gateway.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { JwtModule } from "@nestjs/jwt"; + +import { EmailGateway } from "./email.gateway"; +import { WebSocketAuthService } from "./services/websocket-auth.service"; +import { jwtConfig } from "../../configs/jwt.config"; + +@Module({ + imports: [JwtModule.registerAsync(jwtConfig())], + providers: [EmailGateway, WebSocketAuthService], + exports: [EmailGateway], +}) +export class EmailGatewayModule {} diff --git a/src/modules/email/gateways/email.gateway.ts b/src/modules/email-gateway/email.gateway.ts similarity index 95% rename from src/modules/email/gateways/email.gateway.ts rename to src/modules/email-gateway/email.gateway.ts index 89b6896..97fda9e 100644 --- a/src/modules/email/gateways/email.gateway.ts +++ b/src/modules/email-gateway/email.gateway.ts @@ -10,10 +10,11 @@ import { } from "@nestjs/websockets"; import { Server, Socket } from "socket.io"; -import { WebSocketMessage } from "../../../common/enums/message.enum"; -import { WsExceptionFilter } from "../../../core/filters/ws-exception.filter"; -import { WEBSOCKET_EVENTS, WEBSOCKET_NAMESPACES, WEBSOCKET_ROOMS, WebSocketEvent } from "../constants/email-events.constant"; -import { WebSocketAuthGuard } from "../guards/websocket-auth.guard"; +import { WebSocketAuthService } from "./services/websocket-auth.service"; +import { WebSocketMessage } from "../../common/enums/message.enum"; +import { WsExceptionFilter } from "../../core/filters/ws-exception.filter"; +import { WEBSOCKET_EVENTS, WEBSOCKET_NAMESPACES, WEBSOCKET_ROOMS, WebSocketEvent } from "../email/constants/email-events.constant"; +import { WebSocketAuthGuard } from "../email/guards/websocket-auth.guard"; import { EmailDeletePayload, EmailPayload, @@ -21,9 +22,8 @@ import { EmailStatusPayload, MailboxUpdatePayload, UnreadCountPayload, -} from "../interfaces/email-events.interface"; -import { ConnectedUserInfo } from "../interfaces/websocket.interface"; -import { WebSocketAuthService } from "../services/websocket-auth.service"; +} from "../email/interfaces/email-events.interface"; +import { ConnectedUserInfo } from "../email/interfaces/websocket.interface"; @UsePipes(new ValidationPipe({ transform: true })) @UseFilters(WsExceptionFilter) diff --git a/src/modules/email/services/websocket-auth.service.ts b/src/modules/email-gateway/services/websocket-auth.service.ts similarity index 97% rename from src/modules/email/services/websocket-auth.service.ts rename to src/modules/email-gateway/services/websocket-auth.service.ts index 897509c..62a435c 100644 --- a/src/modules/email/services/websocket-auth.service.ts +++ b/src/modules/email-gateway/services/websocket-auth.service.ts @@ -4,9 +4,9 @@ import { Socket } from "socket.io"; import { WebSocketMessage } from "../../../common/enums/message.enum"; import { ITokenPayload } from "../../auth/interfaces/IToken-payload"; +import { WEBSOCKET_EVENTS, WebSocketEvent } from "../../email/constants/email-events.constant"; +import { AuthenticationResult, WebSocketErrorContext } from "../../email/interfaces/websocket.interface"; import { extractTokenFromClient } from "../../utils/services/extract-token.utils"; -import { WEBSOCKET_EVENTS, WebSocketEvent } from "../constants/email-events.constant"; -import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/websocket.interface"; @Injectable() export class WebSocketAuthService { diff --git a/src/modules/email-utils/email-utils.module.ts b/src/modules/email-utils/email-utils.module.ts new file mode 100644 index 0000000..6a188cf --- /dev/null +++ b/src/modules/email-utils/email-utils.module.ts @@ -0,0 +1,25 @@ +import { BullModule } from "@nestjs/bullmq"; +import { Module } from "@nestjs/common"; + +import { EmailMonitoringProcessor } from "./queue/email-monitoring.processor"; +import { EmailMonitoringService } from "./services/email-monitoring.service"; +import { EmailNotificationService } from "./services/email-notification.service"; +import { MailboxResolverService } from "./services/mailbox-resolver.service"; +import { EMAIL_QUEUE_CONSTANTS } from "../email/constants/email-events.constant"; +import { EmailGatewayModule } from "../email-gateway/email-gateway.module"; +import { MailServerModule } from "../mail-server/mail-server.module"; +import { NotificationModule } from "../notifications/notifications.module"; + +@Module({ + imports: [ + NotificationModule, + MailServerModule, + EmailGatewayModule, + BullModule.registerQueue({ + name: EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE, + }), + ], + providers: [EmailMonitoringService, EmailNotificationService, MailboxResolverService, EmailMonitoringProcessor], + exports: [EmailMonitoringService, EmailNotificationService, MailboxResolverService], +}) +export class EmailUtilsModule {} diff --git a/src/modules/email/queue/email-monitoring.processor.ts b/src/modules/email-utils/queue/email-monitoring.processor.ts similarity index 83% rename from src/modules/email/queue/email-monitoring.processor.ts rename to src/modules/email-utils/queue/email-monitoring.processor.ts index 7f4f215..a70efad 100644 --- a/src/modules/email/queue/email-monitoring.processor.ts +++ b/src/modules/email-utils/queue/email-monitoring.processor.ts @@ -2,9 +2,9 @@ import { Processor, WorkerHost } from "@nestjs/bullmq"; import { Logger } from "@nestjs/common"; import { Job } from "bullmq"; -import { EMAIL_QUEUE_CONSTANTS } from "../constants/email-events.constant"; -import { EmailMonitoringJob, EmailSchedulerJob } from "../interfaces/email-events.interface"; -import { EmailMonitoringService } from "../services/email-monitoring.service"; +import { EMAIL_QUEUE_CONSTANTS } from "../../email/constants/email-events.constant"; +import { EmailMonitoringJob, EmailSchedulerJob } from "../../email/interfaces/email-events.interface"; +import { EmailMonitoringService } from "../../email-utils/services/email-monitoring.service"; @Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE) export class EmailMonitoringProcessor extends WorkerHost { diff --git a/src/modules/email/services/email-monitoring.service.ts b/src/modules/email-utils/services/email-monitoring.service.ts similarity index 98% rename from src/modules/email/services/email-monitoring.service.ts rename to src/modules/email-utils/services/email-monitoring.service.ts index d4bfb52..d6531da 100644 --- a/src/modules/email/services/email-monitoring.service.ts +++ b/src/modules/email-utils/services/email-monitoring.service.ts @@ -5,13 +5,13 @@ import { Queue } from "bullmq"; import { firstValueFrom } from "rxjs"; import { MailboxResolverService } from "./mailbox-resolver.service"; +import { EMAIL_QUEUE_CONSTANTS } from "../../email/constants/email-events.constant"; +import { EmailMonitoringJob, EmailPayload, EmailSchedulerJob } from "../../email/interfaces/email-events.interface"; +import { EmailGateway } from "../../email-gateway/email.gateway"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MailboxEnum } from "../../mailbox/enums/mailbox.enum"; import { NotificationQueue } from "../../notifications/queue/notification.queue"; import { User } from "../../users/entities/user.entity"; -import { EMAIL_QUEUE_CONSTANTS } from "../constants/email-events.constant"; -import { EmailGateway } from "../gateways/email.gateway"; -import { EmailMonitoringJob, EmailPayload, EmailSchedulerJob } from "../interfaces/email-events.interface"; @Injectable() export class EmailMonitoringService implements OnModuleInit { @@ -224,6 +224,7 @@ export class EmailMonitoringService implements OnModuleInit { await this.notificationQueue.addNewEmailNotification(userId, { fromEmail: message.from[0].address, fromName: message.from[0].name, + subject: message.subject, }); await this.emailGateway.notifyNewEmail(userId, emailPayload); diff --git a/src/modules/email/services/email-notification.service.ts b/src/modules/email-utils/services/email-notification.service.ts similarity index 98% rename from src/modules/email/services/email-notification.service.ts rename to src/modules/email-utils/services/email-notification.service.ts index 1b3e115..62d474c 100644 --- a/src/modules/email/services/email-notification.service.ts +++ b/src/modules/email-utils/services/email-notification.service.ts @@ -3,11 +3,7 @@ import { Injectable, Logger } from "@nestjs/common"; import { firstValueFrom } from "rxjs"; import { MailboxResolverService } from "./mailbox-resolver.service"; -import { MailServerService } from "../../mail-server/services/mail-server.service"; -import { MailboxEnum } from "../../mailbox/enums/mailbox.enum"; -import { User } from "../../users/entities/user.entity"; -import { WebSocketEvent } from "../constants/email-events.constant"; -import { EmailGateway } from "../gateways/email.gateway"; +import { WebSocketEvent } from "../../email/constants/email-events.constant"; import { EmailDeletePayload, EmailSentNotificationParams, @@ -17,7 +13,11 @@ import { MailboxUpdateNotificationParams, MailboxUpdatePayload, UnreadCountPayload, -} from "../interfaces/email-events.interface"; +} from "../../email/interfaces/email-events.interface"; +import { EmailGateway } from "../../email-gateway/email.gateway"; +import { MailServerService } from "../../mail-server/services/mail-server.service"; +import { MailboxEnum } from "../../mailbox/enums/mailbox.enum"; +import { User } from "../../users/entities/user.entity"; @Injectable() export class EmailNotificationService { diff --git a/src/modules/email/services/mailbox-resolver.service.ts b/src/modules/email-utils/services/mailbox-resolver.service.ts similarity index 98% rename from src/modules/email/services/mailbox-resolver.service.ts rename to src/modules/email-utils/services/mailbox-resolver.service.ts index 19bd3e1..51b51e0 100644 --- a/src/modules/email/services/mailbox-resolver.service.ts +++ b/src/modules/email-utils/services/mailbox-resolver.service.ts @@ -1,10 +1,10 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { firstValueFrom } from "rxjs"; +import { UserMailboxIds } from "../../email/interfaces/user-mailbox.interface"; import { MailboxInfo } from "../../mail-server/interfaces/mailboxes-response.interface"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MailboxEnum } from "../../mailbox/enums/mailbox.enum"; -import { UserMailboxIds } from "../interfaces/user-mailbox.interface"; @Injectable() export class MailboxResolverService { diff --git a/src/modules/email/email.module.ts b/src/modules/email/email.module.ts index db31202..2bdfa4e 100644 --- a/src/modules/email/email.module.ts +++ b/src/modules/email/email.module.ts @@ -1,57 +1,19 @@ import { MikroOrmModule } from "@mikro-orm/nestjs"; -import { BullModule } from "@nestjs/bullmq"; import { Module } from "@nestjs/common"; -import { JwtModule } from "@nestjs/jwt"; -// import { EmailGatewayController } from "./controllers/email-gateway.controller"; -import { EMAIL_QUEUE_CONSTANTS } from "./constants/email-events.constant"; import { EmailController } from "./email.controller"; -import { EmailGateway } from "./gateways/email.gateway"; -import { EmailMonitoringProcessor } from "./queue/email-monitoring.processor"; import { EmailHeadersService } from "./services/email-headers.service"; -import { EmailMonitoringService } from "./services/email-monitoring.service"; -import { EmailNotificationService } from "./services/email-notification.service"; import { EmailSpamService } from "./services/email-spam.service"; import { EmailService } from "./services/email.service"; -import { MailboxResolverService } from "./services/mailbox-resolver.service"; -import { jwtConfig } from "../../configs/jwt.config"; +import { EmailUtilsModule } from "../email-utils/email-utils.module"; import { MailServerModule } from "../mail-server/mail-server.module"; import { TemplatesModule } from "../templates/templates.module"; -import { WebSocketAuthService } from "./services/websocket-auth.service"; import { User } from "../users/entities/user.entity"; -import { UsersModule } from "../users/users.module"; @Module({ - imports: [ - MailServerModule, - UsersModule, - TemplatesModule, - JwtModule.registerAsync(jwtConfig()), - MikroOrmModule.forFeature([User]), - BullModule.registerQueue({ - name: EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE, - }), - ], + imports: [MailServerModule, TemplatesModule, EmailUtilsModule, MikroOrmModule.forFeature([User])], controllers: [EmailController], - providers: [ - EmailService, - EmailSpamService, - MailboxResolverService, - EmailHeadersService, - EmailGateway, - EmailMonitoringService, - EmailNotificationService, - EmailMonitoringProcessor, - WebSocketAuthService, - ], - exports: [ - EmailService, - EmailSpamService, - MailboxResolverService, - EmailHeadersService, - EmailGateway, - EmailMonitoringService, - EmailNotificationService, - ], + providers: [EmailService, EmailSpamService, EmailHeadersService], + exports: [EmailService, EmailSpamService, EmailHeadersService], }) export class EmailModule {} diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index 515ce08..5340f2c 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -3,10 +3,10 @@ import { FastifyReply } from "fastify"; import { firstValueFrom } from "rxjs"; import { EmailHeadersService } from "./email-headers.service"; -import { EmailNotificationService } from "./email-notification.service"; import { EmailSpamService } from "./email-spam.service"; -import { MailboxResolverService } from "./mailbox-resolver.service"; import { EmailMessage } from "../../../common/enums/message.enum"; +import { EmailNotificationService } from "../../email-utils/services/email-notification.service"; +import { MailboxResolverService } from "../../email-utils/services/mailbox-resolver.service"; import { SearchMessagesMailServerQueryDto } from "../../mail-server/DTO/message.dto"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { TemplateProcessorService } from "../../templates/services/template-processor.service"; diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts index 91d31ac..b7a3388 100755 --- a/src/modules/notifications/interfaces/ISendNotificationData.ts +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -15,4 +15,5 @@ export interface IGenericNotificationData extends IBaseNotificationData { export interface INewEmailNotificationData extends IBaseNotificationData { fromEmail: string; fromName: string; + subject: string; } diff --git a/src/modules/notifications/services/notifications.service.ts b/src/modules/notifications/services/notifications.service.ts index 693bbc2..99e92bc 100755 --- a/src/modules/notifications/services/notifications.service.ts +++ b/src/modules/notifications/services/notifications.service.ts @@ -124,7 +124,7 @@ export class NotificationsService { //************************ */ async createNewEmailNotification(recipientId: string, data: INewEmailNotificationData, em: EntityManager) { - const message = NotificationMessage.NEW_EMAIL_MESSAGE.replace("[subject]", data.newEmail); + const message = NotificationMessage.NEW_EMAIL_MESSAGE.replace("[subject]", data.subject); const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.NEW_EMAIL, recipientId); if (isActive) { return this.createNotification({ title: NotificationMessage.NEW_EMAIL, type: NotifType.NEW_EMAIL, message, recipientId }, em); diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index 49bb49d..1afac26 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -16,10 +16,8 @@ import { Business } from "../../businesses/entities/business.entity"; import { DomainStatus } from "../../domains/enums/domain-status.enum"; import { DomainAutomationService } from "../../domains/services/domain-automation.service"; import { DomainsService } from "../../domains/services/domains.service"; -import { MailboxResolverService } from "../../email/services/mailbox-resolver.service"; import { UpdateUserDto } from "../../mail-server/DTO/update-user.dto"; import { MailServerService } from "../../mail-server/services/mail-server.service"; -import { NotificationQueue } from "../../notifications/queue/notification.queue"; import { PushNotificationService } from "../../notifications/services/push-notification.service"; import { QuotaSyncService } from "../../quota-sync/services/quota-sync.service"; import { UserSettingsService } from "../../settings/services/user-settings.service"; @@ -53,8 +51,6 @@ export class UsersService { private readonly domainAutomationService: DomainAutomationService, private readonly quotaSyncService: QuotaSyncService, private readonly userSettingsService: UserSettingsService, - private readonly mailboxResolver: MailboxResolverService, - private readonly notificationQueue: NotificationQueue, private readonly pushNotificationService: PushNotificationService, ) {}