Skip to content

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

Open
@bourquep

Description

@bourquep

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:

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();
});

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions