Receiving one notification for each tab in browser in foreground with FCM

Viewed 2574

I'm using FCM API to receive push notifications from browser. The firebase-messaging-sw.js works as expected and messaging.setBackgroundMessageHandler fires only once when the web app is in background. However, when the app is in foreground, I'm receiving one notification for each browser tab (if I have the app opened in 3 tabs, I receive 3 notifications). I wonder how should I handle this, since I can't find any reference to this issue. This is the code for FCM messages in the foreground:

import NotificationActionCreators from '../actions/NotificationActionCreators';
import NotificationService from './NotificationService';
import LocalStorageService from './LocalStorageService';
import { FIREBASE_SCRIPT, FCM_URL, FCM_API_KEY, FCM_AUTH_DOMAIN, FCM_PROJECT_ID, FCM_SENDER_ID, PUSH_PUBLIC_KEY } from '../constants/Constants';


class ServiceWorkerService {

constructor() {
    this._messaging = null;
    this._subscriptionData = null;
}

// This function is called once
init() {
    this.loadScript(FIREBASE_SCRIPT, () => this.onFirebaseLoaded());
}

onFirebaseLoaded() {
    // Initialize Firebase
    let config = {
        apiKey: FCM_API_KEY,
        authDomain: FCM_AUTH_DOMAIN,
        projectId: FCM_PROJECT_ID,
        messagingSenderId: FCM_SENDER_ID
    };
    firebase.initializeApp(config);
    this._messaging = firebase.messaging();

    this.requestPermission();

    // Callback fired if Instance ID token is updated.
    this._messaging.onTokenRefresh(() => {
        this._messaging.getToken()
            .then((refreshedToken) => {
                console.log('Token refreshed.');
                NotificationActionCreators.unSubscribe(this._subscriptionData).then(() => {
                    // Indicate that the new Instance ID token has not yet been sent to the
                    // app server.
                    this.setTokenSentToServer(false);
                    // Send Instance ID token to app server.
                    this.sendTokenToServer(refreshedToken);
                }, () => console.log('Error unsubscribing user'));
            })
            .catch(function(err) {
                console.log('Unable to retrieve refreshed token ', err);
            });
    });

    // Handle incoming messages.
    // *** THIS IS FIRED ONCE PER TAB ***
    this._messaging.onMessage(function(payload) {
        console.log("Message received. ", payload);
        const data = payload.data;

        NotificationActionCreators.notify(data);
    });
}

requestPermission() {
    console.log('Requesting permission...');
    return this._messaging.requestPermission()
        .then(() => {
            console.log('Notification permission granted.');
            this.getToken();
        })
        .catch(function(err) {
            console.log('Unable to get permission to notify.', err);
        });
}

getToken() {
    // Get Instance ID token. Initially this makes a network call, once retrieved
    // subsequent calls to getToken will return from cache.
    return this._messaging.getToken()
        .then((currentToken) => {
            if (currentToken) {
                this.sendTokenToServer(currentToken);
            } else {
                // Show permission request.
                console.log('No Instance ID token available. Request permission to generate one.');
                this.setTokenSentToServer(false);
            }
        })
        .catch(function(err) {
            console.log('An error occurred while retrieving token. ', err);
            this.setTokenSentToServer(false);
        });
}

sendTokenToServer(currentToken) {
    const subscriptionData = {
        endpoint: FCM_URL + currentToken,
        platform: 'Web'
    };
    if (!this.isTokenSentToServer()) {
        console.log('Sending token to server...');
        this.updateSubscriptionOnServer(subscriptionData);
    } else {
        console.log('Token already sent to server so won\'t send it again ' +
            'unless it changes');
    }
    this._subscriptionData = subscriptionData;
}

isTokenSentToServer() {
    return LocalStorageService.get('sentToServer') == 1;
}

setTokenSentToServer(sent) {
    LocalStorageService.set('sentToServer', sent ? 1 : 0);
}

updateSubscriptionOnServer(subscriptionData) {
    if (subscriptionData) {
        NotificationActionCreators.subscribe(subscriptionData);
        this.setTokenSentToServer(true);
        this._subscriptionData = subscriptionData;
    } else {
        console.log('Not subscribed');
    }
}

unSubscribe() {
    this.removeSetTokenSentToServer();
    return this._messaging.getToken()
        .then((currentToken) => {
            return this._messaging.deleteToken(currentToken)
                .then(() => {
                    console.log('Token deleted.');
                    return NotificationActionCreators.unSubscribe(this._subscriptionData);
                })
                .catch(function(err) {
                    console.log('Unable to delete token. ', err);
                    return new Promise(function(resolve, reject) {
                        reject(error)
                    });
                });
       })
        .catch(function(err) {
            console.log('Error retrieving Instance ID token. ', err);
            return new Promise(function(resolve, reject) {
                reject(error)
            });
        });
    }
}

removeSetTokenSentToServer() {
    LocalStorageService.remove('sentToServer');
}

loadScript = function (url, callback) {
    let head = document.getElementsByTagName('head')[0];
    let script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;

    script.onload = callback;

    // Fire the loading
    head.appendChild(script);
}
}

Is there any way to show the notification just for the first tab found?

3 Answers

One way is to create a unique variable per tab, say Math.random() or (new Date()).getMilliseconds() and store that on the server with the token. Now the server can target each tab by attaching the variable to the message, with each tab checking the message variable before acting.

To reduces odds of targeting a closed tab, send the variable up with each request, so the server always targets the latest one.

use document.hidden to detect active tab

if (!document.hidden) {
    NotificationActionCreators.notify(data);
}
Related