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
-
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); } }; -
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
-
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.jsis copied to your build output - Most React build tools automatically copy files from
public/
🐛 Troubleshooting
Common Issues
-
Service Worker Not Registering:
// Check in browser console navigator.serviceWorker.getRegistrations().then((registrations) => { console.log("Active registrations:", registrations); }); -
Push Subscription Fails:
- Verify VAPID keys are correct
- Check browser permissions
- Ensure HTTPS is used
-
Notifications Not Appearing:
- Check browser notification settings
- Verify notification permission is granted
- Check if "Do Not Disturb" is enabled
-
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
- Validate Permissions: Always check notification permission before attempting to subscribe
- Error Handling: Implement proper error boundaries and user feedback
- Token Management: Securely store and refresh authentication tokens
- Content Security Policy: Configure CSP headers to allow service worker execution
- 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
- Lazy Loading: Load push notification components only when needed
- Service Worker Caching: Cache API responses for better offline experience
- Bundle Splitting: Separate push notification code into its own chunk
- 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! 🚀