firebase-js-sdk
firebase-js-sdk copied to clipboard
[FCM] Chrome always displays "This site has been updated in the background" notification
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-pwa1.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();
});
I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
In the Chrome dev tools, here's what I see in the Application - Push Messaging section (after enabling recording):
And here's the log output from my service worker, confirming that it is properly registered and receiving the notification in onBackgroundMessage:
@looptheloop88 This has been labeled as a question, but it's actually a bug report.
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.
I am no longer able to reproduce this issue... which is strange because I did not change anything in the code.
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!
Well... a teammate just reproduced, also with Chrome on macOS.
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.
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!
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.