Skip to content

Push Notification Integration

Complete guide for frontend engineers to implement Firebase Cloud Messaging (FCM) push notifications in web and mobile applications.


Overview

The push notification system enables real-time notifications to users across web browsers and mobile devices using Firebase Cloud Messaging (FCM).

Key Features:

  • Web Push Notifications - Browser notifications via Service Workers
  • Cross-Platform Support - Web, Android, iOS compatible
  • Foreground & Background - Handle notifications in both states
  • Device Registration - Automatic token management and refresh
  • Multi-Device Support - Same user, multiple devices
  • Automatic Delivery - Triggered by booking events, reminders, payments

Integration Flow:

  1. Setup Firebase - Configure Firebase SDK in your app
  2. Register Service Worker - Handle background notifications
  3. Generate FCM Token - Get unique device token from Firebase
  4. Register Device - Store token with backend API
  5. Receive Notifications - Handle foreground and background messages
  6. Token Refresh - Update token when it changes

Prerequisites

Firebase Project Configuration

Your app needs these Firebase configuration values (provided by backend team):

const firebaseConfig = {
    apiKey: "AIzaSyBRYaAn9jQctShoJ3ZmmYj18p-IFHCkoB8",
    authDomain: "skilled-compass-218404.firebaseapp.com",
    projectId: "skilled-compass-218404",
    storageBucket: "skilled-compass-218404.firebasestorage.app",
    messagingSenderId: "740443181568",
    appId: "1:740443181568:web:1e975c24ccb34014a7fee5"
};

// VAPID key for web push
const VAPID_KEY = 'BNeGVaRaXsZlb-mqZ2fKQQh3Bpjg72HCdKHLgpBohBjJn1s9d_iXjjSHXqWdsgIXUkuiOY2Lo5Q6IgYa9iAIlow';

Required Packages

NPM Installation:

npm install firebase

CDN (Browser):

<script type="module">
  import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.0/firebase-app.js';
  import { getMessaging, getToken, onMessage } from 'https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging.js';
</script>

Step 1: Initialize Firebase

React/Next.js Setup

lib/firebase.js:

import { initializeApp, getApps } from 'firebase/app';
import { getMessaging, getToken, onMessage, isSupported } from 'firebase/messaging';

const firebaseConfig = {
    apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
    authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
    projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
    storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
    appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID
};

const VAPID_KEY = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY;

// Initialize Firebase (singleton)
let app;
let messaging;

export function initializeFirebase() {
    if (typeof window === 'undefined') return null; // Server-side guard

    if (!getApps().length) {
        app = initializeApp(firebaseConfig);
    } else {
        app = getApps()[0];
    }

    return app;
}

export async function getFirebaseMessaging() {
    if (typeof window === 'undefined') return null;

    // Check if messaging is supported
    const supported = await isSupported();
    if (!supported) {
        console.warn('Firebase Messaging is not supported in this browser');
        return null;
    }

    if (!messaging) {
        const app = initializeFirebase();
        messaging = getMessaging(app);
    }

    return messaging;
}

export { getToken, onMessage, VAPID_KEY };

Vue.js Setup

plugins/firebase.js:

import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage, isSupported } from 'firebase/messaging';

const firebaseConfig = {
    apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
    authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
    projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
    storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
    appId: import.meta.env.VITE_FIREBASE_APP_ID
};

const VAPID_KEY = import.meta.env.VITE_FIREBASE_VAPID_KEY;

let messaging = null;

export async function initFirebaseMessaging() {
    const supported = await isSupported();
    if (!supported) return null;

    const app = initializeApp(firebaseConfig);
    messaging = getMessaging(app);
    return messaging;
}

export { messaging, getToken, onMessage, VAPID_KEY };

Step 2: Service Worker Setup

The service worker handles background notifications when the app/tab is not focused.

Create Service Worker

public/firebase-messaging-sw.js:

// Import Firebase scripts (compat version for service workers)
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js');

// Firebase configuration
firebase.initializeApp({
    apiKey: "AIzaSyBRYaAn9jQctShoJ3ZmmYj18p-IFHCkoB8",
    authDomain: "skilled-compass-218404.firebaseapp.com",
    projectId: "skilled-compass-218404",
    storageBucket: "skilled-compass-218404.firebasestorage.app",
    messagingSenderId: "740443181568",
    appId: "1:740443181568:web:1e975c24ccb34014a7fee5"
});

const messaging = firebase.messaging();

// Handle background messages
messaging.onBackgroundMessage((payload) => {
    console.log('[SW] Background message received:', payload);

    const notificationTitle = payload.notification?.title || 'New Notification';
    const notificationOptions = {
        body: payload.notification?.body || '',
        icon: '/icons/notification-icon.png',
        badge: '/icons/badge-icon.png',
        tag: payload.data?.notification_type || 'default',
        data: payload.data || {},
        vibrate: [200, 100, 200],
        requireInteraction: true, // Keep visible until user interacts
        actions: [
            { action: 'view', title: 'View Details' },
            { action: 'dismiss', title: 'Dismiss' }
        ]
    };

    self.registration.showNotification(notificationTitle, notificationOptions);
});

// Handle notification click
self.addEventListener('notificationclick', (event) => {
    event.notification.close();

    const action = event.action;
    const data = event.notification.data || {};

    if (action === 'dismiss') return;

    // Determine URL to open based on notification type
    let urlToOpen = '/';

    switch (data.notification_type) {
        case 'booking_confirmation':
        case 'booking_reminder':
        case 'booking_rescheduled':
            urlToOpen = data.appointment_id
                ? `/appointments/${data.appointment_id}`
                : '/appointments';
            break;
        case 'payment_receipt':
            urlToOpen = data.payment_id
                ? `/payments/${data.payment_id}`
                : '/payments';
            break;
        default:
            urlToOpen = data.action_url || '/notifications';
    }

    event.waitUntil(
        clients.matchAll({ type: 'window', includeUncontrolled: true })
            .then((clientList) => {
                // Focus existing window if available
                for (const client of clientList) {
                    if (client.url.includes(self.location.origin) && 'focus' in client) {
                        client.postMessage({
                            type: 'NOTIFICATION_CLICKED',
                            data: data,
                            url: urlToOpen
                        });
                        return client.focus();
                    }
                }
                // Open new window
                if (clients.openWindow) {
                    return clients.openWindow(urlToOpen);
                }
            })
    );
});

// Service worker lifecycle
self.addEventListener('install', () => {
    self.skipWaiting();
});

self.addEventListener('activate', (event) => {
    event.waitUntil(clients.claim());
});

Register Service Worker

In your main app initialization:

async function registerServiceWorker() {
    if (!('serviceWorker' in navigator)) {
        console.warn('Service Workers not supported');
        return null;
    }

    try {
        const registration = await navigator.serviceWorker.register(
            '/firebase-messaging-sw.js',
            { scope: '/' }
        );

        // Wait for service worker to be ready
        if (registration.installing) {
            await new Promise((resolve) => {
                registration.installing.addEventListener('statechange', (e) => {
                    if (e.target.state === 'activated') resolve();
                });
            });
        }

        await navigator.serviceWorker.ready;
        console.log('Service Worker registered successfully');
        return registration;
    } catch (error) {
        console.error('Service Worker registration failed:', error);
        return null;
    }
}

Step 3: Request Notification Permission

Permission Request

async function requestNotificationPermission() {
    if (!('Notification' in window)) {
        return { granted: false, reason: 'not_supported' };
    }

    if (Notification.permission === 'granted') {
        return { granted: true, reason: 'already_granted' };
    }

    if (Notification.permission === 'denied') {
        return { granted: false, reason: 'denied' };
    }

    // Request permission
    const permission = await Notification.requestPermission();

    return {
        granted: permission === 'granted',
        reason: permission
    };
}

React Hook Example

hooks/useNotificationPermission.js:

import { useState, useCallback } from 'react';

export function useNotificationPermission() {
    const [permission, setPermission] = useState(
        typeof window !== 'undefined' ? Notification.permission : 'default'
    );
    const [loading, setLoading] = useState(false);

    const requestPermission = useCallback(async () => {
        setLoading(true);
        try {
            const result = await Notification.requestPermission();
            setPermission(result);
            return result === 'granted';
        } catch (error) {
            console.error('Permission request failed:', error);
            return false;
        } finally {
            setLoading(false);
        }
    }, []);

    return {
        permission,
        isGranted: permission === 'granted',
        isDenied: permission === 'denied',
        isDefault: permission === 'default',
        requestPermission,
        loading
    };
}

Step 4: Generate FCM Token

Token Generation

import { getFirebaseMessaging, getToken, VAPID_KEY } from './firebase';

async function generateFCMToken() {
    try {
        // Check permission
        if (Notification.permission !== 'granted') {
            throw new Error('Notification permission not granted');
        }

        // Get messaging instance
        const messaging = await getFirebaseMessaging();
        if (!messaging) {
            throw new Error('Firebase Messaging not supported');
        }

        // Register service worker
        const registration = await navigator.serviceWorker.ready;

        // Get FCM token
        const token = await getToken(messaging, {
            vapidKey: VAPID_KEY,
            serviceWorkerRegistration: registration
        });

        if (!token) {
            throw new Error('Failed to generate FCM token');
        }

        console.log('FCM Token generated:', token.substring(0, 20) + '...');
        return token;

    } catch (error) {
        console.error('FCM token generation failed:', error);
        throw error;
    }
}

Complete Initialization Flow

async function initializePushNotifications() {
    // Step 1: Check/request permission
    const { granted, reason } = await requestNotificationPermission();

    if (!granted) {
        return {
            success: false,
            error: `Permission ${reason}`,
            token: null
        };
    }

    // Step 2: Register service worker
    const swRegistration = await registerServiceWorker();
    if (!swRegistration) {
        return {
            success: false,
            error: 'Service worker registration failed',
            token: null
        };
    }

    // Step 3: Generate FCM token
    try {
        const token = await generateFCMToken();
        return {
            success: true,
            error: null,
            token: token
        };
    } catch (error) {
        return {
            success: false,
            error: error.message,
            token: null
        };
    }
}

Step 5: Register Device with Backend

After obtaining the FCM token, register it with the backend to enable push notification delivery.

Full API Documentation

For complete API documentation including all endpoints, request/response schemas, error handling, and best practices, see Device Management.

Quick Reference

Endpoint Method Purpose
/api/v1/devices/register POST Register FCM token (Staff)
/api/v1/devices/register/customer POST Register FCM token (Customer)

JavaScript Implementation

class PushNotificationService {
    constructor(apiBaseUrl, getAuthToken) {
        this.apiBaseUrl = apiBaseUrl;
        this.getAuthToken = getAuthToken; // Function that returns JWT token
        this.fcmToken = null;
        this.deviceId = null;
    }

    async registerDevice(fcmToken, isCustomer = false) {
        const endpoint = isCustomer
            ? '/api/v1/devices/register/customer'
            : '/api/v1/devices/register';

        const deviceInfo = this.getDeviceInfo();

        const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${await this.getAuthToken()}`
            },
            body: JSON.stringify({
                fcm_token: fcmToken,
                platform: 'web',
                device_name: deviceInfo.deviceName,
                app_version: deviceInfo.appVersion,
                device_info: deviceInfo
            })
        });

        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.detail || 'Device registration failed');
        }

        const data = await response.json();
        this.fcmToken = fcmToken;
        this.deviceId = data.id;

        // Store token locally for refresh detection
        localStorage.setItem('fcm_token', fcmToken);
        localStorage.setItem('device_id', data.id);

        return data;
    }

    getDeviceInfo() {
        const ua = navigator.userAgent;
        return {
            deviceName: this.getDeviceName(ua),
            appVersion: '1.0.0', // Your app version
            browser: this.getBrowserName(ua),
            os: this.getOSName(ua),
            screen: `${window.screen.width}x${window.screen.height}`,
            language: navigator.language,
            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
        };
    }

    getBrowserName(ua) {
        if (ua.includes('Chrome')) return 'Chrome';
        if (ua.includes('Firefox')) return 'Firefox';
        if (ua.includes('Safari')) return 'Safari';
        if (ua.includes('Edge')) return 'Edge';
        return 'Unknown';
    }

    getOSName(ua) {
        if (ua.includes('Windows')) return 'Windows';
        if (ua.includes('Mac')) return 'macOS';
        if (ua.includes('Linux')) return 'Linux';
        if (ua.includes('Android')) return 'Android';
        if (ua.includes('iOS')) return 'iOS';
        return 'Unknown';
    }

    getDeviceName(ua) {
        const browser = this.getBrowserName(ua);
        const os = this.getOSName(ua);
        return `${browser} on ${os}`;
    }
}

Step 6: Handle Incoming Notifications

Foreground Notifications

When the app is in the foreground (tab is active):

import { onMessage } from 'firebase/messaging';

function setupForegroundNotifications(messaging) {
    onMessage(messaging, (payload) => {
        console.log('Foreground notification received:', payload);

        // Extract notification data
        const { notification, data } = payload;
        const title = notification?.title || 'New Notification';
        const body = notification?.body || '';

        // Option 1: Show in-app notification (Toast/Snackbar)
        showInAppNotification({
            title,
            body,
            type: data?.notification_type,
            data
        });

        // Option 2: Also show browser notification
        if (Notification.permission === 'granted') {
            new Notification(title, {
                body,
                icon: '/icons/notification-icon.png',
                tag: data?.notification_type || 'default',
                data
            });
        }

        // Option 3: Update notification badge/counter
        updateNotificationBadge();

        // Option 4: Refresh relevant data
        handleNotificationAction(data);
    });
}

function handleNotificationAction(data) {
    const type = data?.notification_type;

    switch (type) {
        case 'booking_confirmation':
        case 'booking_reminder':
        case 'booking_rescheduled':
        case 'booking_cancelled':
            // Refresh appointments list
            refreshAppointments();
            break;
        case 'payment_receipt':
            // Refresh payment history
            refreshPayments();
            break;
        default:
            // Refresh notification list
            refreshNotifications();
    }
}

Background Notifications

Background notifications are handled by the service worker (see Step 2).

Listen for Service Worker Messages

// Listen for notification clicks from service worker
navigator.serviceWorker.addEventListener('message', (event) => {
    if (event.data?.type === 'NOTIFICATION_CLICKED') {
        const { data, url } = event.data;

        // Handle navigation
        if (url) {
            window.location.href = url;
        }

        // Handle specific notification types
        handleNotificationClick(data);
    }
});

function handleNotificationClick(data) {
    const type = data?.notification_type;

    switch (type) {
        case 'booking_confirmation':
            // Navigate to appointment details
            router.push(`/appointments/${data.appointment_id}`);
            break;
        case 'booking_reminder':
            // Show appointment reminder modal
            showReminderModal(data);
            break;
        case 'payment_receipt':
            // Navigate to receipt
            router.push(`/payments/${data.payment_id}`);
            break;
    }
}

Step 7: Token Refresh Handling

FCM tokens can expire or change. Handle token refresh:

import { onMessage } from 'firebase/messaging';

class TokenRefreshHandler {
    constructor(pushService) {
        this.pushService = pushService;
        this.lastToken = localStorage.getItem('fcm_token');
    }

    async checkAndRefreshToken() {
        try {
            const currentToken = await generateFCMToken();

            if (currentToken !== this.lastToken) {
                console.log('FCM token changed, re-registering device...');
                await this.pushService.registerDevice(currentToken);
                this.lastToken = currentToken;
            }
        } catch (error) {
            console.error('Token refresh check failed:', error);
        }
    }

    // Call periodically (e.g., every hour or on app focus)
    startPeriodicCheck(intervalMs = 3600000) {
        setInterval(() => this.checkAndRefreshToken(), intervalMs);

        // Also check on window focus
        window.addEventListener('focus', () => this.checkAndRefreshToken());
    }
}

Device Management APIs

For complete device management API documentation, see Device Management.

Available Endpoints

Endpoint Method Purpose
/devices/register POST Register FCM token (Staff)
/devices/register/customer POST Register FCM token (Customer)
/devices GET List user's devices (Staff)
/devices/customer GET List user's devices (Customer)
/devices/{id} PUT Update device (Staff)
/devices/customer/{id} PUT Update device (Customer)
/devices/{id} DELETE Unregister device (Staff)
/devices/customer/{id} DELETE Unregister device (Customer)

The Device Management documentation includes:

  • Full request/response schemas
  • Error handling and common errors
  • Device lifecycle diagrams
  • Token refresh handling
  • Best practices for registration and logout
  • Frontend integration examples (JavaScript, React)

Customer Notification Endpoints

Customers can view their notification history and preferences.

Get Customer Notifications

Endpoint

GET /api/v1/customer/notifications

Authentication: Required (Customer JWT)

Query Parameters

Parameter Type Description
status string Filter: unread, read, all
type string Filter by notification type
skip integer Pagination offset
limit integer Results per page (max: 100)

Response

{
  "items": [
    {
      "id": "507f1f77bcf86cd799439020",
      "notification_type": "booking_confirmation",
      "title": "Booking Confirmed!",
      "body": "Your appointment at Salon Cantik has been confirmed",
      "is_read": false,
      "data": {
        "appointment_id": "507f1f77bcf86cd799439030",
        "service_names": "Hair Cut, Styling"
      },
      "created_at": "2025-01-15T10:00:00Z"
    }
  ],
  "total": 25,
  "unread_count": 5,
  "skip": 0,
  "limit": 20
}

Mark Notification as Read

Endpoint

PUT /api/v1/customer/notifications/{notification_id}/read

Authentication: Required (Customer JWT)

Response

{
  "success": true,
  "message": "Notification marked as read"
}

Mark All as Read

Endpoint

PUT /api/v1/customer/notifications/read-all

Authentication: Required (Customer JWT)

Response

{
  "success": true,
  "count": 5,
  "message": "5 notifications marked as read"
}

Complete React Implementation

Push Notification Provider

contexts/PushNotificationContext.js:

import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { initializeFirebase, getFirebaseMessaging, getToken, onMessage, VAPID_KEY } from '../lib/firebase';
import { useAuth } from './AuthContext';

const PushNotificationContext = createContext(null);

export function PushNotificationProvider({ children }) {
    const { token: authToken, isCustomer } = useAuth();
    const [permission, setPermission] = useState('default');
    const [fcmToken, setFcmToken] = useState(null);
    const [deviceId, setDeviceId] = useState(null);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    const [notifications, setNotifications] = useState([]);

    // Check initial permission
    useEffect(() => {
        if (typeof window !== 'undefined' && 'Notification' in window) {
            setPermission(Notification.permission);
        }
    }, []);

    // Initialize push notifications when user logs in
    useEffect(() => {
        if (authToken && permission === 'granted') {
            initializePush();
        }
    }, [authToken, permission]);

    const requestPermission = useCallback(async () => {
        if (!('Notification' in window)) {
            setError('Notifications not supported');
            return false;
        }

        const result = await Notification.requestPermission();
        setPermission(result);
        return result === 'granted';
    }, []);

    const initializePush = useCallback(async () => {
        if (!authToken) return;

        setLoading(true);
        setError(null);

        try {
            // 1. Register service worker
            await navigator.serviceWorker.register('/firebase-messaging-sw.js');
            await navigator.serviceWorker.ready;

            // 2. Get Firebase messaging
            const messaging = await getFirebaseMessaging();
            if (!messaging) throw new Error('Messaging not supported');

            // 3. Get FCM token
            const token = await getToken(messaging, {
                vapidKey: VAPID_KEY,
                serviceWorkerRegistration: await navigator.serviceWorker.ready
            });

            if (!token) throw new Error('Failed to get FCM token');

            // 4. Register with backend
            const endpoint = isCustomer
                ? '/api/v1/devices/register/customer'
                : '/api/v1/devices/register';

            const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${endpoint}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${authToken}`
                },
                body: JSON.stringify({
                    fcm_token: token,
                    platform: 'web',
                    device_name: `${getBrowser()} on ${getOS()}`
                })
            });

            if (!response.ok) throw new Error('Device registration failed');

            const data = await response.json();
            setFcmToken(token);
            setDeviceId(data.id);

            // 5. Setup foreground handler
            onMessage(messaging, handleForegroundMessage);

            // 6. Listen for SW messages
            navigator.serviceWorker.addEventListener('message', handleSWMessage);

        } catch (err) {
            setError(err.message);
            console.error('Push init failed:', err);
        } finally {
            setLoading(false);
        }
    }, [authToken, isCustomer]);

    const handleForegroundMessage = useCallback((payload) => {
        const notification = {
            id: Date.now(),
            title: payload.notification?.title,
            body: payload.notification?.body,
            type: payload.data?.notification_type,
            data: payload.data,
            timestamp: new Date(),
            isRead: false
        };

        setNotifications(prev => [notification, ...prev]);

        // Show toast notification
        if (window.showToast) {
            window.showToast({
                title: notification.title,
                message: notification.body,
                type: 'info'
            });
        }
    }, []);

    const handleSWMessage = useCallback((event) => {
        if (event.data?.type === 'NOTIFICATION_CLICKED') {
            // Handle navigation or action
            console.log('Notification clicked:', event.data);
        }
    }, []);

    const unregisterDevice = useCallback(async () => {
        if (!deviceId || !authToken) return;

        const endpoint = isCustomer
            ? `/api/v1/devices/customer/${deviceId}`
            : `/api/v1/devices/${deviceId}`;

        await fetch(`${process.env.NEXT_PUBLIC_API_URL}${endpoint}`, {
            method: 'DELETE',
            headers: { 'Authorization': `Bearer ${authToken}` }
        });

        setFcmToken(null);
        setDeviceId(null);
    }, [deviceId, authToken, isCustomer]);

    const value = {
        permission,
        fcmToken,
        deviceId,
        loading,
        error,
        notifications,
        isEnabled: permission === 'granted' && !!fcmToken,
        requestPermission,
        initializePush,
        unregisterDevice,
        clearNotifications: () => setNotifications([])
    };

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

export function usePushNotifications() {
    const context = useContext(PushNotificationContext);
    if (!context) {
        throw new Error('usePushNotifications must be used within PushNotificationProvider');
    }
    return context;
}

// Helpers
function getBrowser() {
    const ua = navigator.userAgent;
    if (ua.includes('Chrome')) return 'Chrome';
    if (ua.includes('Firefox')) return 'Firefox';
    if (ua.includes('Safari')) return 'Safari';
    return 'Browser';
}

function getOS() {
    const ua = navigator.userAgent;
    if (ua.includes('Windows')) return 'Windows';
    if (ua.includes('Mac')) return 'macOS';
    if (ua.includes('Linux')) return 'Linux';
    return 'Unknown';
}

Notification Bell Component

components/NotificationBell.js:

import { useState } from 'react';
import { usePushNotifications } from '../contexts/PushNotificationContext';

export function NotificationBell() {
    const {
        permission,
        isEnabled,
        notifications,
        requestPermission,
        initializePush,
        loading
    } = usePushNotifications();

    const [showDropdown, setShowDropdown] = useState(false);
    const unreadCount = notifications.filter(n => !n.isRead).length;

    const handleEnableNotifications = async () => {
        const granted = await requestPermission();
        if (granted) {
            await initializePush();
        }
    };

    return (
        <div className="notification-bell">
            <button
                onClick={() => setShowDropdown(!showDropdown)}
                className="bell-button"
            >
                <BellIcon />
                {unreadCount > 0 && (
                    <span className="badge">{unreadCount}</span>
                )}
            </button>

            {showDropdown && (
                <div className="dropdown">
                    {!isEnabled ? (
                        <div className="enable-prompt">
                            <p>Enable push notifications to stay updated</p>
                            <button
                                onClick={handleEnableNotifications}
                                disabled={loading || permission === 'denied'}
                            >
                                {loading ? 'Enabling...' : 'Enable Notifications'}
                            </button>
                            {permission === 'denied' && (
                                <p className="denied-message">
                                    Notifications blocked. Please enable in browser settings.
                                </p>
                            )}
                        </div>
                    ) : (
                        <div className="notification-list">
                            {notifications.length === 0 ? (
                                <p className="empty">No notifications yet</p>
                            ) : (
                                notifications.slice(0, 5).map(n => (
                                    <NotificationItem key={n.id} notification={n} />
                                ))
                            )}
                        </div>
                    )}
                </div>
            )}
        </div>
    );
}

function NotificationItem({ notification }) {
    return (
        <div className={`notification-item ${notification.isRead ? '' : 'unread'}`}>
            <div className="title">{notification.title}</div>
            <div className="body">{notification.body}</div>
            <div className="time">{formatTime(notification.timestamp)}</div>
        </div>
    );
}

Best Practices

For Setup & Initialization

DO:

  • Initialize Firebase only once (singleton pattern)
  • Check isSupported() before using Firebase Messaging
  • Request permission explicitly with user action (button click)
  • Handle all permission states (granted, denied, default)
  • Store FCM token locally for refresh detection
  • Register service worker before getting FCM token

DON'T:

  • Auto-request permission on page load (bad UX, may get denied)
  • Assume notifications are supported in all browsers
  • Skip service worker registration
  • Ignore token refresh handling
  • Store sensitive data in notification payloads

For User Experience

DO:

  • Explain why notifications are useful before requesting
  • Provide easy way to enable/disable notifications
  • Show in-app notifications for foreground messages
  • Use notification badges/counters
  • Allow users to manage notification preferences
  • Handle notification clicks with appropriate navigation

DON'T:

  • Send too many notifications (notification fatigue)
  • Use notifications for marketing without consent
  • Ignore notification grouping for multiple messages
  • Show duplicate notifications (foreground + background)
  • Leave stale notifications in the UI

For Security

DO:

  • Validate authentication before device registration
  • Use HTTPS for all API calls
  • Clear FCM token on user logout
  • Unregister device when user logs out
  • Handle token refresh securely

DON'T:

  • Store FCM tokens in cookies
  • Send sensitive data in notification body
  • Trust client-provided user IDs
  • Skip authentication for device endpoints

Notification Types Reference

Type When Triggered Typical Content
booking_confirmation Appointment booked "Your booking is confirmed for [date] at [time]"
booking_reminder Before appointment "Reminder: Your appointment is in [X hours]"
booking_cancelled Appointment cancelled "Your appointment has been cancelled"
booking_rescheduled Time changed "Your appointment has been rescheduled to [new time]"
payment_receipt Payment completed "Payment of [amount] received"
welcome Customer registered "Welcome to [tenant name]!"

Troubleshooting

Common Issues

Issue Cause Solution
"Permission denied" User blocked notifications Show instructions to enable in browser settings
"Service Worker 404" Wrong path Ensure firebase-messaging-sw.js is in public/ folder
"No active Service Worker" SW not ready Wait for navigator.serviceWorker.ready
"Invalid VAPID key" Wrong key Use correct VAPID key from Firebase Console
Token is null Permission not granted Request permission first
Notifications not showing SW not receiving Check SW console for errors
"messaging/token-subscribe-failed" Invalid credentials Verify Firebase config

Debug Checklist

  1. Browser Console:
  2. Check for Firebase initialization errors
  3. Verify FCM token is generated
  4. Look for service worker errors

  5. Service Worker Console:

  6. Open DevTools → Application → Service Workers
  7. Check for registration errors
  8. Verify background message handler

  9. Network Tab:

  10. Verify device registration API call succeeds
  11. Check authentication headers
  12. Confirm response status

  13. Backend Logs:

  14. Check for FCM send errors
  15. Verify notification was queued
  16. Look for token validation failures

Browser Support

Browser Web Push Support
Chrome Full support
Firefox Full support
Edge Full support
Safari Limited (requires APNS)
Opera Full support
Mobile Chrome Full support
Mobile Firefox Full support

API Reference Summary

Device Management Endpoints

See Device Management for full API documentation.

Endpoint Method Auth Purpose
/devices/register POST Staff JWT Register FCM token (staff)
/devices/register/customer POST Customer JWT Register FCM token (customer)
/devices GET Staff JWT List staff devices
/devices/customer GET Customer JWT List customer devices
/devices/{id} PUT Staff JWT Update device
/devices/customer/{id} PUT Customer JWT Update customer device
/devices/{id} DELETE Staff JWT Unregister device
/devices/customer/{id} DELETE Customer JWT Unregister customer device

Customer Notification Endpoints

Endpoint Method Auth Purpose
/customer/notifications GET Customer JWT Get notification history
/customer/notifications/{id}/read PUT Customer JWT Mark as read
/customer/notifications/read-all PUT Customer JWT Mark all as read

Document Description
Device Management Complete device/FCM token API documentation
Notification Management Backend notification sending and scheduling
Notification Settings Template customization and preferences
Customer Authentication Customer login and JWT token handling
Staff Authentication Staff login and JWT token handling

Next Steps:

  1. Set up Firebase configuration in your app
  2. Create and register the service worker
  3. Implement permission request UI
  4. Add device registration after login
  5. Handle foreground and background notifications
  6. Add notification preferences to user settings