firebase-js-sdk icon indicating copy to clipboard operation
firebase-js-sdk copied to clipboard

[FCM] Chrome always displays "This site has been updated in the background" notification

Open bourquep opened this issue 6 months ago • 9 comments

Operating System

macOS Sequoia 15.5

Environment (if applicable)

Chrome 136.0.7103.115

Firebase SDK Version

11.6.1

Firebase SDK Product(s)

Messaging

Project Tooling

  • React (19.1.0) app built with Vite 6.3.4
  • Service worker built with vite-plugin-pwa 1.0.0

Detailed Problem Description

I am using FCM to send display push notifications to a React web app. The notifications are properly displayed, but they are always accompanied by an extra notification that says This site has been updated in the background.

So far, I have tested in Chrome and Safari and only Chrome exhibits this behaviour.

I have tried:

Service Worker Configuration Real Notification "Site Updated" Notification Notes
Empty firebase-messaging-sw.js file ❌ Not displayed ⚠️ Displayed Real notification completely suppressed
Service worker with self.registration.showNotification() in onBackgroundMessage ⚠️ Displayed twice ⚠️ Displayed Duplicate real notifications
Service worker without onBackgroundMessage handler ✅ Displayed once ⚠️ Displayed Code included below

I know it has been suggested in other similar bug reports to use data-only notifications, but I don't want to do that as we plan on using the FCM campaigns dashboard to send some notifications to our users. Also, I believe that data notifications would not work on mobile devices (iOS).

This issue has been reported in various forms over the years, with no clear resolution or workaround:

  • #6764
  • #6478
  • #5603

Steps and code to reproduce issue

Firebase initialization code
import { captureException } from '@sentry/react';
import { FeatureFlagService, UserIdentificationService } from '@shared/services';
import { deleteToken, getMessaging, getToken, Messaging, onMessage } from 'firebase/messaging';
import { action, autorun, computed, makeObservable, observable } from 'mobx';
import { FirebaseService } from './FirebaseService';
import { DisplayNotificationAuthorizationState, PushNotificationsService } from './PushNotificationsService';
import { StudyoEnvironmentService } from './StudyoEnvironmentService';

export class WebFirebasePushNotificationsService implements PushNotificationsService {
  private readonly _messaging: Messaging;
  private _lastRegisteredUserId?: string;
  @observable private _authorizationState: DisplayNotificationAuthorizationState;

  @computed
  get authorizationState() {
    return this._authorizationState;
  }

  constructor(
    firebaseService: FirebaseService,
    private readonly _environmentService: StudyoEnvironmentService,
    private readonly _featureFlagService: FeatureFlagService,
    private readonly _userIdentificationService: UserIdentificationService
  ) {
    makeObservable(this);

    this._messaging = getMessaging(firebaseService.app);
    this._authorizationState = 'not_supported';

    void this.updateAuthorizationState();

    autorun(async () => {
      await this.registerDevice(this._userIdentificationService.userId);
    });

    onMessage(this._messaging, (payload) => {
      console.log('Received foreground message:', payload);

      new Notification(payload.notification?.title ?? 'Studyo', {
        body: payload.notification?.body ?? 'Studyo Notification',
        icon: payload.notification?.icon ?? '/favicon-32x32.png',
        tag: `foreground-${payload.messageId}`,
        data: payload.data
      });
    });
  }

  async requestPermissionIfNeeded() {
    if (this.authorizationState !== 'unknown') {
      return;
    }

    try {
      const permission = await Notification.requestPermission();
      await this.updateAuthorizationState();
      if (permission === 'granted') {
        await this.registerDevice(this._userIdentificationService.userId);
      }
    } catch (error) {
      console.error('Error requesting permission:', error);
      captureException(error);
    }
  }

  @action
  private async updateAuthorizationState() {
    await this._featureFlagService.isReady;
    if (!this._featureFlagService.isEnabled('push-notifications')) {
      this._authorizationState = 'not_supported';
      return;
    }

    try {
      switch (Notification.permission) {
        case 'granted':
          this._authorizationState = 'granted';
          break;

        case 'denied':
          this._authorizationState = 'denied';
          break;

        default:
          this._authorizationState = 'unknown';
          break;
      }
    } catch (error) {
      console.error('Error checking notification authorization state:', error);
      this._authorizationState = 'not_supported';
    }
  }

  private async registerDevice(userId?: string) {
    if (this.authorizationState !== 'granted') {
      return;
    }

    if (this._lastRegisteredUserId && this._lastRegisteredUserId !== userId) {
      await this.unregisterDevice(this._lastRegisteredUserId);
    }

    if (!userId) {
      return;
    }

    try {
      const token = await this.getToken();
      console.log(`Registering user ${userId} with device push notification token: ${token}`);
      // TODO: Call API to register device token
      this._lastRegisteredUserId = userId;
    } catch (error) {
      console.error('FCM push notification token not available:', error);
      captureException(error);
    }
  }

  private async unregisterDevice(userId: string) {
    if (this.authorizationState !== 'granted') {
      return;
    }

    try {
      const token = await this.getToken();
      console.log(`Unregistering user ${userId} with device push notification token: ${token}`);
      // TODO: Call API to unregister device token
      this._lastRegisteredUserId = undefined;
      await deleteToken(this._messaging);
    } catch (error) {
      console.error('FCM push notification token not available:', error);
      captureException(error);
    }
  }

  private async getToken() {
    const token = await getToken(this._messaging, {
      vapidKey: this._environmentService.firebaseConfig.vapidKey
    });
    return token;
  }
}
Service worker code
import { initializeApp } from 'firebase/app';
import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw';

importScripts('/environment.js');

declare const self: ServiceWorkerGlobalScope;

const app = initializeApp(self.STUDYO_ENV.firebaseConfig);
const messaging = getMessaging(app);

// eslint-disable-next-line @typescript-eslint/no-misused-promises
onBackgroundMessage(messaging, (payload) => {
  console.log('FCM onBackgroundMessage', payload);
  return Promise.resolve();
});

bourquep avatar May 30 '25 15:05 bourquep

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

google-oss-bot avatar May 30 '25 15:05 google-oss-bot

In the Chrome dev tools, here's what I see in the Application - Push Messaging section (after enabling recording):

Image

And here's the log output from my service worker, confirming that it is properly registered and receiving the notification in onBackgroundMessage:

Image

bourquep avatar May 30 '25 15:05 bourquep

@looptheloop88 This has been labeled as a question, but it's actually a bug report.

bourquep avatar May 30 '25 19:05 bourquep

The question label is part of our system for ensuring that the issue is logged into our internal bug tracking system and does not mean we don't believe it's a bug.

hsubox76 avatar May 30 '25 20:05 hsubox76

I am no longer able to reproduce this issue... which is strange because I did not change anything in the code.

bourquep avatar Jun 01 '25 15:06 bourquep

Ok, I'll mark this as needs-info so that it closes in a week or two, but if it occurs again then reply here and it'll keep the issue open. Thanks!

DellaBitta avatar Jun 02 '25 14:06 DellaBitta

Well... a teammate just reproduced, also with Chrome on macOS.

bourquep avatar Jun 02 '25 17:06 bourquep

Hi @bourquep thank you for reaching out to us. We’ve looked into the issue but were not able to replicate the reported behavior on our end. I'm not familiar with the implementation details of vite-plugin-pwa and to better assist you, could you please provide a Minimal, Complete, and Verifiable Example (MCVE) that reproduces the issue? This would help us better understand the context and pinpoint the cause more effectively.

jbalidiong avatar Jun 03 '25 11:06 jbalidiong

Hey @bourquep. We need more information to resolve this issue but there hasn't been an update in 5 weekdays. I'm marking the issue as stale and if there are no new updates in the next 5 days I will close it automatically.

If you have more information that will help us get to the bottom of this, just add a comment!

google-oss-bot avatar Jun 10 '25 01:06 google-oss-bot

Since there haven't been any recent updates here, I am going to close this issue.

@bourquep if you're still experiencing this problem and want to continue the discussion just leave a comment here and we are happy to re-open this.

google-oss-bot avatar Jun 17 '25 01:06 google-oss-bot