'use client' import { useState, useEffect } from 'react' // Enhanced demo implementation with real service worker registration class DemoClientPushManager { private vapidPublicKey: string private serviceWorkerPath: string private apiEndpoint: string constructor(vapidPublicKey: string, options: { serviceWorkerPath?: string; apiEndpoint?: string } = {}) { this.vapidPublicKey = vapidPublicKey this.serviceWorkerPath = options.serviceWorkerPath || '/sw.js' this.apiEndpoint = options.apiEndpoint || '/api/push-notifications' } public isSupported(): boolean { if (typeof window === 'undefined') return false return ( 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window ) } public getPermissionStatus(): NotificationPermission { if (typeof window === 'undefined' || typeof Notification === 'undefined') return 'default' return Notification.permission } public async requestPermission(): Promise { if (!this.isSupported()) { throw new Error('Push notifications are not supported') } return await Notification.requestPermission() } public async registerServiceWorker(): Promise { if (!this.isSupported()) { throw new Error('Service workers are not supported') } try { const registration = await navigator.serviceWorker.register(this.serviceWorkerPath) console.log('Service worker registered:', registration) // Wait for service worker to be ready await navigator.serviceWorker.ready return registration } catch (error) { console.error('Service worker registration failed:', error) throw error } } public async subscribe(): Promise { const permission = await this.requestPermission() if (permission !== 'granted') { throw new Error('Notification permission not granted') } const registration = await this.registerServiceWorker() // For demo purposes, we'll simulate subscription without actual VAPID keys // In production, you would use real VAPID keys here try { const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey), }) console.log('Push subscription:', subscription) return { endpoint: subscription.endpoint, keys: { p256dh: this.arrayBufferToBase64(subscription.getKey('p256dh')!), auth: this.arrayBufferToBase64(subscription.getKey('auth')!), }, } } catch (error) { console.warn('Real push subscription failed, simulating for demo:', error) // Return simulated subscription for demo return { endpoint: 'demo-endpoint', keys: { p256dh: 'demo-key', auth: 'demo-auth' } } } } public async isSubscribed(): Promise { if (!this.isSupported()) return false try { const registration = await navigator.serviceWorker.getRegistration() if (!registration) return false const subscription = await registration.pushManager.getSubscription() return subscription !== null } catch { return false } } public async unsubscribe(): Promise { try { const registration = await navigator.serviceWorker.getRegistration() if (!registration) return const subscription = await registration.pushManager.getSubscription() if (subscription) { await subscription.unsubscribe() } } catch (error) { console.error('Unsubscribe failed:', error) } } public async sendTestNotification(): Promise { // Send a test notification using the service worker const registration = await navigator.serviceWorker.getRegistration() if (!registration) { throw new Error('Service worker not registered') } // Simulate receiving a push message if (registration.active) { registration.active.postMessage({ type: 'TEST_NOTIFICATION', payload: { title: 'Test Notification', body: 'This is a test notification from the demo!', icon: '/icons/notification-icon.png', badge: '/icons/notification-badge.png', data: { url: '/admin/collections/notifications', notificationId: 'demo-' + Date.now() } } }) } // Also show a direct notification for testing if (Notification.permission === 'granted') { new Notification('Direct Test Notification', { body: 'This notification was sent directly from JavaScript', icon: '/icons/notification-icon.png', tag: 'direct-test' }) } } private 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 } private arrayBufferToBase64(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer) const binary = Array.from(bytes).map(b => String.fromCharCode(b)).join('') return window.btoa(binary) } } // Available channels (should match the configuration in payload.config.ts) const AVAILABLE_CHANNELS = [ { id: 'general', name: 'General Notifications', description: 'General updates and announcements', defaultEnabled: true }, { id: 'orders', name: 'Order Updates', description: 'Order status changes and shipping notifications', defaultEnabled: true }, { id: 'products', name: 'Product Updates', description: 'New products, restocks, and price changes', defaultEnabled: false }, { id: 'marketing', name: 'Marketing & Promotions', description: 'Special offers, sales, and promotional content', defaultEnabled: false }, ] export default function DemoPage() { const [isSupported, setIsSupported] = useState(false) const [isSubscribed, setIsSubscribed] = useState(false) const [permission, setPermission] = useState('default') const [pushManager, setPushManager] = useState(null) const [loading, setLoading] = useState(false) const [selectedChannels, setSelectedChannels] = useState( AVAILABLE_CHANNELS.filter(channel => channel.defaultEnabled).map(channel => channel.id) ) useEffect(() => { // Use the real VAPID public key from environment const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || 'BNde-uFUkQB5BweFbOt_40Tn3xZahMop2JKT8kqRn4UqMMinieguHmVCTxwN_qfM-jZ0YFpVpIk3CWehlXcTl8A' const manager = new DemoClientPushManager(vapidPublicKey) setPushManager(manager) setIsSupported(manager.isSupported()) setPermission(manager.getPermissionStatus()) if (manager.isSupported()) { manager.isSubscribed().then(setIsSubscribed) } }, []) const handleSubscribe = async () => { if (!pushManager) return setLoading(true) try { const subscription = await pushManager.subscribe() // Save the subscription to Payload's database using the plugin's API endpoint const response = await fetch('/api/push-notifications/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subscription: subscription, user: 'customer@example.com', // Associate with the demo customer user userAgent: navigator.userAgent, channels: selectedChannels, }), }) if (response.ok) { setIsSubscribed(true) setPermission('granted') alert('Successfully subscribed to push notifications!\n\nSubscription saved to database.') } else { const error = await response.text() throw new Error(`Failed to save subscription: ${error}`) } } catch (error) { console.error('Failed to subscribe:', error) alert('Failed to subscribe to push notifications: ' + (error as Error).message) } setLoading(false) } const handleUnsubscribe = async () => { if (!pushManager) return setLoading(true) try { await pushManager.unsubscribe() // Remove the subscription from Payload's database const response = await fetch('/api/push-notifications/unsubscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ user: 'customer@example.com', // Associate with the demo customer user }), }) if (response.ok) { setIsSubscribed(false) alert('Successfully unsubscribed from push notifications') } else { const error = await response.text() console.warn('Failed to remove subscription from database:', error) setIsSubscribed(false) alert('Unsubscribed from browser, but may still be in database') } } catch (error) { console.error('Failed to unsubscribe:', error) alert('Failed to unsubscribe from push notifications: ' + (error as Error).message) } setLoading(false) } const handleTestNotification = async () => { if (!pushManager) return setLoading(true) try { await pushManager.sendTestNotification() alert('Test notification sent! Check your browser notifications.') } catch (error) { console.error('Failed to send test notification:', error) alert('Failed to send test notification: ' + (error as Error).message) } setLoading(false) } return (

Payload Notifications Plugin Demo

🔔 Web Push Notifications

{!isSupported ? (
❌ Push notifications are not supported in this browser
) : (

Status: {isSubscribed ? '✅ Subscribed' : '❌ Not subscribed'}

Permission: {permission}

{!isSubscribed && (

📢 Select Notification Channels

Choose which types of notifications you want to receive:

{AVAILABLE_CHANNELS.map(channel => ( ))}
)}
{!isSubscribed ? ( ) : ( <> )}
)}

📱 Admin Panel Features

The notifications plugin adds the following to your Payload admin panel:

  • Notifications Collection: Create and manage notifications with rich text content
  • Push Subscriptions Collection: View and manage user push notification subscriptions (check here after subscribing!)
  • 📢 Channel-Based Subscriptions: Users can subscribe to specific notification channels (General, Orders, Products, Marketing)
  • Read/Unread Tracking: Monitor which notifications have been read
  • User Targeting: Send notifications to specific users
  • 🎯 Automatic Push Notifications: Push notifications are sent automatically when notifications are created!

🚀 Try Automatic Push Notifications

⚠️ Important: You must be signed in to subscribe to push notifications. The subscription associates with your user account.

Step 1: Sign in to the admin panel first (dev@payloadcms.com / test)

Step 2: Return here and subscribe to push notifications above ↑

Step 3: Go to the admin panel and create a new notification

Step 4: Set the recipient to "customer@example.com" (the test user)

Step 5: Choose a notification channel (General, Orders, Products, or Marketing) - must match your subscription

Step 6: Save the notification and watch for an automatic push notification! 🎉

💡 How it works: When you create a notification in the admin panel, the plugin automatically:
  • Extracts the title and message content
  • Finds all push subscriptions for the recipient
  • Sends push notifications to their devices
  • Handles errors gracefully without breaking the notification creation

🚀 API Endpoints

The plugin automatically creates these API endpoints for web push:

  • POST /api/push-notifications/subscribe - Subscribe to push notifications
  • POST /api/push-notifications/unsubscribe - Unsubscribe from push notifications
  • GET /api/push-notifications/vapid-public-key - Get VAPID public key
  • POST /api/push-notifications/send - Send notification to user
  • POST /api/push-notifications/test - Send test notification (admin only)

💡 Getting Started

  1. Generate VAPID keys: npx web-push generate-vapid-keys
  2. Add the keys to your .env file
  3. Create a service worker at /public/sw.js
  4. Use the client-side utilities to manage subscriptions
  5. Send notifications programmatically or via the admin panel

📋 Sample Data

This demo includes:

  • Sample users (admin and customer)
  • Sample notifications demonstrating channels
  • Push subscription management with channel filtering
  • Automatic push notification hooks

Login: dev@payloadcms.com / test
Admin Panel: /admin

) }