File "firebase-messaging-sw.js"

Full Path: /home/trinadezambia/public_html/student_panel/public/firebase-messaging-sw.js
File size: 15.12 KB
MIME-type: text/plain
Charset: utf-8

/**
 * Firebase Cloud Messaging Service Worker - Simplified Version
 *
 * This service worker handles push notifications from Firebase.
 * It keeps things simple to check if messages are received.
 *
 * Version: 2.3 - Fixed duplicate notifications
 * - Use message ID as tag to prevent duplicates
 * - Coordinate with foreground handler
 * - Added renotify: false to prevent re-alerting
 */

// Service Worker Version
// const SW_VERSION = '2.3';

// Import Firebase scripts from CDN
importScripts(
  'https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js'
);
importScripts(
  'https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js'
);

// Firebase configuration
const firebaseConfig = {
  apiKey: 'xxxxxxxxxxxxxxxxxx',
  authDomain: 'xxxxxxxxxxxxxxxxxx',
  projectId: 'xxxxxxxxxxxxxxxxxx',
  storageBucket: 'xxxxxxxxxxxxxxxxxx',
  messagingSenderId: 'xxxxxxxxxxxxxxxxxx',
  appId: 'xxxxxxxxxxxxxxxxxx',
  measurementId: 'xxxxxxxxxxxxxxxxxx',
};

// console.log('[SW] Service worker loaded - Version:', SW_VERSION);
// console.log('[SW] Firebase config:', firebaseConfig);

// Initialize Firebase
try {
  firebase.initializeApp(firebaseConfig);
  // console.log('[SW] ✅ Firebase initialized');
} catch (error) {
  console.error('[SW] ❌ Error initializing Firebase:', error);
}

// Get Firebase Messaging instance
// We still instantiate messaging so Firebase keeps binding the SW to FCM.
// const messaging = firebase.messaging();
// self.__firebaseMessagingInstance = messaging;
// console.log('[SW] ✅ Messaging instance created');

/* -----------------------------------------------------
   Service Worker Lifecycle Events
   CRITICAL: These ensure immediate activation in production
------------------------------------------------------ */
// Force service worker to activate immediately (skip waiting)
self.addEventListener('install', function () {
  // Skip waiting means the new service worker will activate immediately
  // instead of waiting for all tabs to close
  self.skipWaiting();
});

// Claim all clients immediately when service worker activates
self.addEventListener('activate', function (event) {
  // clients.claim() makes the service worker take control of all pages
  // immediately, even if they were loaded before the service worker activated
  event.waitUntil(self.clients.claim());
});

/* -----------------------------------------------------
   Helper: Build redirect URL based on notification type
   Handles all backend notification types
   Backend now only sends 'type' field
------------------------------------------------------ */

/* -----------------------------------------------------
   Helper: Build student redirect URL by notification type
   Matches src/lib/firebase/notificationRedirect.ts
------------------------------------------------------ */
const getNotificationRedirectUrl = (notificationData = {}) => {
  const type = notificationData?.type?.toLowerCase() || '';
  const extraType = notificationData?.exam_type?.toLowerCase() || '';
  // console.log('[SW] Notification data:', notificationData);
  // console.log('[SW] Notification type:', type);
  // console.log('[SW] Extra type:', extraType);
  // Announcement ───────────────────────────────────────────────
  if (type.includes('announcement') ||
      type === 'announcement created' ||
      type === 'announcement updated') {
    return '/student/dashboard';
  }

  // Lesson topic ───────────────────────────────────────────────
  if (type === 'lesson topic created' ||
      type === 'lesson topic updated' ||
      type === 'lesson' ||
      type === 'topic') {
    const sid = data.subject_id;
    if (sid && Number.isInteger(Number(sid))) {
      return `/student/subjects/detail?id=${sid}`;
    }
    return '/student/subjects';
  }

  if (type === 'assignment') {
    const assignmentId = notificationData.assignment_id;
    const classSubjectId = notificationData.class_subject_id;
    if (assignmentId && classSubjectId) {
      return `/student/assignments?assignment_id=${assignmentId}&class_subject_id=${classSubjectId}`;
    }
    return '/student/assignments';
  }
    if (type === 'message') {
    const receiverId = notificationData.receiver_id;
    if (receiverId) {
      return `/student/chats?receiver_id=${receiverId}`;
    }
    return '/student/chats';
  }

  if (type === 'payment' || type === 'fees_paid' || type === 'fee-reminder') {
    return '/student/transactions';
  }

  if (type === 'message') {
    const receiverId = notificationData.receiver_id;
    if (receiverId) {
      return `/student/chats?receiver_id=${receiverId}`;
    }
    return '/student/chats';
  }

  if (type === 'payment' || type === 'fees_paid' || type === 'fee-reminder') {
    return '/student/transactions';
  }

  if (type === 'lesson') {
    const subjectId = notificationData.subject_id;
    if (subjectId) {
      return `/student/subjects/detail?id=${subjectId}`;
    }
    return '/student/dashboard';
  }

  // if (type === 'topic') {
  //   const subjectId = notificationData.subject_id;
  //   const lessonId = notificationData.lesson_id;
  //   if (subjectId && lessonId) {
  //     return `/student/subjects/lesson?subjectId=${subjectId}&lessonId=${lessonId}`;
  //   }
  //   if (subjectId) {
  //     return `/student/subjects/detail?id=${subjectId}`;
  //   }
  //   return '/student/subjects';
  // }

  if (type === 'exam') {
    const examId = notificationData.exam_id;
    // console.log('[SW] Exam ID:', examId);
    // console.log('[SW] Extra type:', extraType);
    // console.log(typeof extraType);

    if (extraType === 'online' && examId) {
      // console.log('[SW] Returning online exam URL with tab parameter');
      return `/student/exams?tab=online&exam_id=${examId}`;
    }

    if (extraType === 'offline' && examId) {
      // console.log('[SW] Returning offline exam URL');
      return `/student/exams/offline/detail?id=${examId}`;
    }

    // Fallback to general exams page if exam_type or exam_id is missing
    // console.log('[SW] Falling back to general exams page');
    return '/student/exams';
  }

  if (type === 'exam result' || type === 'result') {
    const resultId = notificationData.result_id || notificationData.exam_id;
    if (resultId) {
      return `/student/result?result_id=${resultId}`;
    }
    return '/student/result';
  }

  if (type === 'exam_timetable_created') {
    const examId = notificationData.exam_id;
    if (examId) {
      return `/student/exams?exam_id=${examId}`;
    }
    return '/student/exams';
  }

  if (type === 'attendance') {
    return '/student/attendance';
  }

  if (type === 'diary') {
    return '/student/diary';
  }

  if (type === 'leave') {
    return '/student/notifications';
  }

  if (type === 'class section') {
    const subjectId = notificationData.subject_id;
    if (subjectId) {
      return `/student/subjects/detail?id=${subjectId}`;
    }
    return '/student/subjects';
  }

  if (type === 'transport' || type === 'transportation') {
    return '/student/transportation';
  }

  if (type === 'notification' || type === 'general' || type === 'custom') {
    return '/student/notifications';
  }

  return '/student/notifications';
};

/* -----------------------------------------------------
   FCM Background Notification Handler
------------------------------------------------------ */
self.addEventListener('push', function (event) {
  if (!event.data) {
    return;
  }

  try {
    const payload = event.data.json();
    const data = payload.data || {};

    const title = payload.notification?.title || data.title || 'Notification';
    const body = payload.notification?.body || data.body || '';
    const icon = data.icon || data.image || '/favicon.ico';

    // Build final redirect URL
    const redirectUrl = getNotificationRedirectUrl(data);
    // console.log('[SW] Redirect URL:', redirectUrl);

    // Calculate absolute URL that will be used on click
    const origin = self.location.origin;
    let absoluteRedirectUrl = redirectUrl;
    if (
      redirectUrl.startsWith('http://') ||
      redirectUrl.startsWith('https://')
    ) {
      absoluteRedirectUrl = redirectUrl;
    } else {
      const cleanUrl = redirectUrl.startsWith('/')
        ? redirectUrl
        : '/' + redirectUrl;
      absoluteRedirectUrl = origin + cleanUrl;
    }

    // console.log('[SW] Absolute Redirect URL:', absoluteRedirectUrl);
    // console.log('[SW] Origin:', origin);

    const options = {
      body,
      icon,
      data: {
        url: redirectUrl,
        absoluteUrl: absoluteRedirectUrl,
        originalData: data,
      },
      requireInteraction: false,
    };

    // Check if the app is currently focused
    event.waitUntil(
      clients
        .matchAll({
          type: 'window',
          includeUncontrolled: true,
        })
        .then((windowClients) => {
          // Check if there is a focused window
          for (const client of windowClients) {
            if (client.focused) {
              // App is in foreground!

              // Send message to client to update UI (chat, badges, etc.)
              client.postMessage(payload);

              // Check if we are on the chat page
              // If so, suppress the notification
              if (client.url && client.url.includes('/chats')) {
                // console.log('[SW] On chat page, suppressing notification');
                return;
              }

              // If not on chat page, we fall through to show notification
              // console.log('[SW] Focused but not on chat page, showing notification');
              break; // Break loop to show notification
            }
          }

          // Show notification (if no focused client on chat page)
          return self.registration.showNotification(title, options);
        })
    );
  } catch (e) {
    console.error('🔔 [SW] Error processing push notification:', e);
  }
});

/* -----------------------------------------------------
   Notification Click → Focus existing tab or open new
------------------------------------------------------ */
self.addEventListener('notificationclick', function (event) {
  // console.log('[SW] Notification clicked');
  const notificationData = event.notification.data || {};
  // console.log('[SW] Notification data:', notificationData);

  let url = notificationData.url || '/';
  const originalData = notificationData.originalData || {};

  // console.log('[SW] Initial URL from notification data:', url);
  // console.log('[SW] Original data:', originalData);

  // If URL is missing or just "/", recalculate from original data
  // This handles cases where the URL wasn't stored correctly
  if (!url || url === '/' || url === '') {
    // console.log('[SW] URL is missing or empty, recalculating from original data');
    if (originalData && Object.keys(originalData).length > 0) {
      url = getNotificationRedirectUrl(originalData);
      // console.log('[SW] Recalculated URL:', url);
    } else {
      url = '/';
      // console.log('[SW] No original data, using default URL:', url);
    }
  }

  event.notification.close();

  // Build full absolute URL (required for openWindow)
  const origin = self.location.origin;

  // Check if URL is already absolute
  let fullUrl = url;
  if (url.startsWith('http://') || url.startsWith('https://')) {
    fullUrl = url;
  } else {
    // Build absolute URL from relative path
    const cleanUrl = url.startsWith('/') ? url : '/' + url;
    fullUrl = origin + cleanUrl;
  }

  // Validate URL format
  if (!fullUrl.startsWith('http://') && !fullUrl.startsWith('https://')) {
    const cleanUrl = url.startsWith('/') ? url : '/' + url;
    fullUrl = origin + cleanUrl;
  }

  // console.log('[SW] Final full URL for navigation:', fullUrl);
  // console.log('[SW] Origin:', origin);

  event.waitUntil(
    clients
      .matchAll({
        type: 'window',
        includeUncontrolled: true,
      })
      .then((windowClients) => {
        // Check for existing tabs from this origin
        let foundExistingTab = false;
        for (const client of windowClients) {
          if (client.url && client.url.startsWith(origin)) {
            foundExistingTab = true;
            // console.log('[SW] Found existing tab:', client.url);
            // console.log('[SW] Navigating to:', fullUrl);
            // console.log('[SW] Relative URL:', url);

            // Focus the existing tab
            if (client.focus) {
              client.focus();
            }

            // Send navigation message to the tab with RELATIVE URL for smooth navigation
            client.postMessage({
              action: 'navigate',
              url: url, // Send relative URL, not absolute
              fullUrl: fullUrl, // Also send full URL as backup
            });

            // Also send with type format for PushNotification component
            client.postMessage({
              type: 'NAVIGATE',
              url: url, // Send relative URL, not absolute
              fullUrl: fullUrl, // Also send full URL as backup
            });

            // console.log('[SW] Posted navigation message to client');
            return Promise.resolve();
          }
        }

        // No existing tab found - open new window
        if (!foundExistingTab) {
          // console.log(
          //   '[SW] No existing tab found, opening new window with URL:',
          //   fullUrl
          // );
          // Use clients.openWindow - this MUST be called within user gesture
          // and MUST return a promise that resolves to the new window
          const openWindowPromise = clients.openWindow(fullUrl);

          return openWindowPromise
            .then((newClient) => {
              if (newClient) {
                return newClient;
              } else {
                // Try one more time as fallback
                return clients.openWindow(fullUrl).then((retryClient) => {
                  return retryClient;
                });
              }
            })
            .catch((error) => {
              console.error('🔔 [SW] Error opening new window:', error);
              // Try one final time
              try {
                const finalAttempt = clients.openWindow(fullUrl);
                if (finalAttempt && finalAttempt.then) {
                  return finalAttempt.then((finalClient) => {
                    return finalClient;
                  });
                }
                return finalAttempt;
              } catch (e) {
                console.error('🔔 [SW] Final attempt failed:', e);
                throw e;
              }
            });
        }
      })
      .catch((error) => {
        console.error('🔔 [SW] Error in notification click handler:', error);
        // Last resort: try to open window directly (may not work due to context)
        try {
          if (clients && clients.openWindow) {
            const result = clients.openWindow(fullUrl);
            if (result && result.then) {
              return result.then((client) => {
                return client;
              });
            }
            return result;
          }
        } catch (e) {
          console.error('🔔 [SW] Failed to open window in catch block:', e);
        }
      })
  );
});