Files
dmail-api/REACT_WEB_PUSH_README.md
T

32 KiB

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

# 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:

// 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:

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<void>;
  unsubscribe: () => Promise<void>;
  testNotification: () => Promise<void>;
  updateSettings: (enabled: boolean) => Promise<void>;
  clearError: () => void;
}

3. API Service

Create src/services/pushNotificationService.ts:

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<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
    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<any> {
    return this.makeRequest("/users/push-subscriptions", {
      method: "POST",
      body: JSON.stringify(data),
    });
  }

  async removeSubscription(endpoint: string): Promise<any> {
    return this.makeRequest("/users/push-subscriptions", {
      method: "DELETE",
      body: JSON.stringify({ endpoint }),
    });
  }

  async updateSettings(enabled: boolean): Promise<any> {
    return this.makeRequest("/users/push-notifications/settings", {
      method: "PATCH",
      body: JSON.stringify({ enabled }),
    });
  }

  async getNotificationInfo(): Promise<PushNotificationInfo> {
    return this.makeRequest("/users/push-notifications/info");
  }

  async testNotification(data: TestNotificationRequest = {}): Promise<any> {
    return this.makeRequest("/users/push-notifications/test", {
      method: "POST",
      body: JSON.stringify(data),
    });
  }
}

export default PushNotificationService;

4. Utility Helper

Create src/utils/vapidHelper.ts:

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:

import { useEffect, useState, useCallback } from "react";

interface UseServiceWorkerReturn {
  registration: ServiceWorkerRegistration | null;
  isLoading: boolean;
  error: string | null;
  register: () => Promise<void>;
}

export function useServiceWorker(scriptURL = "/sw.js"): UseServiceWorkerReturn {
  const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(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:

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<void>;
  unsubscribe: () => Promise<void>;
  testNotification: () => Promise<void>;
  checkSubscriptionStatus: () => Promise<void>;
  clearError: () => void;
}

export function usePushNotifications({ apiService }: UsePushNotificationsProps): UsePushNotificationsReturn {
  const [isSubscribed, setIsSubscribed] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [subscriptionCount, setSubscriptionCount] = useState(0);
  const [permission, setPermission] = useState<NotificationPermission>("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<void> => {
    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<void> => {
    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<void> => {
    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:

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<PushNotificationContextValue | undefined>(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<void> => {
    await apiService.updateSettings(enabled);
  };

  const value: PushNotificationContextValue = {
    isSupported,
    isSubscribed,
    isLoading,
    error,
    subscriptionCount,
    subscribe,
    unsubscribe,
    testNotification,
    updateSettings,
    clearError,
  };

  return <PushNotificationContext.Provider value={value}>{children}</PushNotificationContext.Provider>;
}

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:

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 (
      <button className={`${className} opacity-50 cursor-not-allowed`} disabled title="Push notifications are not supported in this browser">
        {unsupportedText}
      </button>
    );
  }

  return (
    <div className="space-y-2">
      <button className={className} onClick={handleClick} disabled={isLoading}>
        {isLoading ? loadingText : isSubscribed ? enabledText : disabledText}
      </button>

      {error && <div className="text-red-600 text-sm">{error}</div>}
    </div>
  );
}

Notification Settings Component

Create src/components/NotificationSettings.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 (
      <div className="p-4 bg-yellow-100 border border-yellow-400 rounded-md">
        <h3 className="text-lg font-semibold text-yellow-800">Push Notifications Not Supported</h3>
        <p className="text-yellow-700">
          Your browser doesn't support push notifications. Please use a modern browser like Chrome, Firefox, Safari 16+, or Edge.
        </p>
      </div>
    );
  }

  return (
    <div className="p-6 bg-white rounded-lg shadow-md">
      <h3 className="text-xl font-semibold mb-4">📬 Email Notifications</h3>

      <div className="space-y-4">
        <div className="flex items-center justify-between">
          <div>
            <p className="font-medium">Desktop Notifications</p>
            <p className="text-sm text-gray-600">Get notified instantly when new emails arrive, even when the app is closed.</p>
          </div>

          <div className="flex items-center space-x-2">
            <button
              onClick={isSubscribed ? unsubscribe : subscribe}
              disabled={isLoading}
              className={`px-4 py-2 rounded-md font-medium ${
                isSubscribed ? "bg-red-600 hover:bg-red-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"
              } disabled:opacity-50 disabled:cursor-not-allowed`}
            >
              {isLoading ? "Loading..." : isSubscribed ? "Disable" : "Enable"}
            </button>
          </div>
        </div>

        {error && (
          <div className="p-3 bg-red-100 border border-red-400 rounded-md">
            <p className="text-red-700">{error}</p>
            <button onClick={clearError} className="text-red-600 hover:text-red-800 text-sm underline mt-1">
              Dismiss
            </button>
          </div>
        )}

        {isSubscribed && (
          <div className="space-y-3">
            <div className="p-3 bg-green-100 border border-green-400 rounded-md">
              <p className="text-green-700">
                 Notifications are enabled! You have {subscriptionCount} active subscription{subscriptionCount !== 1 ? "s" : ""}.
              </p>
            </div>

            <button onClick={handleTest} className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md font-medium">
              Send Test Notification
            </button>
          </div>
        )}

        <div className="text-xs text-gray-500">
          <p>
            🔒 Your notification preferences are stored securely and can be changed at any time. Notifications work even when your browser is closed.
          </p>
        </div>
      </div>
    </div>
  );
}

Notification Prompt Component

Create src/components/PushNotificationPrompt.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 (
    <div className="fixed bottom-4 right-4 max-w-sm bg-white border border-gray-300 rounded-lg shadow-lg z-50">
      <div className="p-4">
        <div className="flex items-start space-x-3">
          <div className="text-2xl">📧</div>
          <div className="flex-1">
            <h4 className="font-semibold text-gray-900">Stay Updated with Email Notifications</h4>
            <p className="text-sm text-gray-600 mt-1">Get instant notifications when new emails arrive, even when the app is closed.</p>
          </div>
          <button onClick={handleDismiss} className="text-gray-400 hover:text-gray-600" aria-label="Close">
            
          </button>
        </div>

        <div className="flex space-x-2 mt-4">
          <button
            onClick={handleEnable}
            disabled={isLoading}
            className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-md text-sm font-medium disabled:opacity-50"
          >
            {isLoading ? "Enabling..." : "Enable Notifications"}
          </button>
          <button onClick={handleDismiss} className="px-3 py-2 text-gray-600 hover:text-gray-800 text-sm font-medium">
            Not Now
          </button>
        </div>
      </div>
    </div>
  );
}

9. App Integration

Update your main App.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 (
    <PushNotificationProvider apiBaseURL={apiBaseURL} getAuthToken={getAuthToken}>
      <div className="App">
        {/* Your existing app content */}
        <header className="App-header">
          <h1>Dmail - Email Client</h1>
        </header>

        <main>
          {/* Your existing routes and components */}

          {/* Add notification settings to your settings page */}
          <div className="container mx-auto p-4">
            <NotificationSettings />
          </div>
        </main>

        {/* Notification prompt - shows automatically for new users */}
        <PushNotificationPrompt />
      </div>
    </PushNotificationProvider>
  );
}

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:

/* 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:

    // 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:

# 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:

    // 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:

// 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:

// 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


🎉 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! 🚀