# 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! ๐Ÿš€