1
votes

The Problem:

I have been unable to use Firebase (Google) Cloud Functions to collect and utilize device tokens for the cloud messaging feature.

Context:

I am a self-taught android-Java developer and have no JavaScript experience. Despite that, I believe I have code that should work and am not sure what the problem is. To my understanding, it could be one of three things:

  1. Somehow my Firebase Realtime Database references are being called incorrectly and I am not retrieving data as expected.

  2. I may need to use Promises to wait for all calls to be made before proceeding, however I don't really understand how I would incorporate that into the code I have.

  3. I may be using multiple return statements incorrectly (which I am also fuzzy on).

My error message on the Firebase Realtime Database console is as follows:

@firebase/database: FIREBASE WARNING: Exception was thrown by user callback. Error: Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array. at FirebaseMessagingError.FirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:42:28) at FirebaseMessagingError.PrefixedFirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:88:28) at new FirebaseMessagingError (/srv/node_modules/firebase-admin/lib/utils/error.js:254:16) at Messaging.validateRegistrationTokensType (/srv/node_modules/firebase-admin/lib/messaging/messaging.js:729:19) at Messaging.sendToDevice (/srv/node_modules/firebase-admin/lib/messaging/messaging.js:328:14) at admin.database.ref.once.snapshot (/srv/index.js:84:12) at onceCallback (/srv/node_modules/@firebase/database/dist/index.node.cjs.js:4933:51) at /srv/node_modules/@firebase/database/dist/index.node.cjs.js:4549:22 at exceptionGuard (/srv/node_modules/@firebase/database/dist/index.node.cjs.js:698:9) at EventList.raise (/srv/node_modules/@firebase/database/dist/index.node.cjs.js:9684:17)

The above indicates I am not retrieving data either at all or by the time the return is called. My JavaScript function code is:

'use strict'; 

const admin = require('firebase-admin');
admin.initializeApp();

exports.pushNotification = functions.database.ref('/Chat Messages/{chatId}/{pushID}').onCreate((snapshot, context) => {

    const valueObject = snapshot.after.val(); 
   
        return admin.database().ref(`/Chat Basics/${valueObject.chatKey}/Chat Users`).once('value', statusSnapshot => {
            
                var index = 0;    
                  var totalkeys = statusSnapshot.numChildren();
                var msgIDs = [];
        
                statusSnapshot.forEach(msg=>{

                    msgIDs.push(msg.key.toString());
                      
                        if(index === totalkeys - 1){
          
                            const payload = {
                                notification : {
                                        title: valueObject.userName,
                                              body: valueObject.message,
                                              sound: "default"
                                }
                              }
                    
                              sendNotificationPayload(valueObject.uid, payload);
                        }
        
        
                    index++;
        
        
                  });

        });

});

function sendNotificationPayload(uid, payload){

    admin.database()
    .ref(`/User Token Data/${uid}`)
    .once('value', snapshot=> {
        
        var tokens = [];

        //if(!snapshot.exists())return;

        snapshot.forEach(item =>{
            tokens.push(item.val())
        });


        admin.messaging()
        .sendToDevice(tokens, payload)
        .then(res => {
            return console.log('Notification sent')
        })
        .catch(err => {
            return console.log('Error in sending notification = '+err)
        });

    });

}

This code is mostly inspired by what was said to be a working example here from another Stack Overflow question here. I have successfully tested sending a notification to a single device by manually copying a device token into my function, so the function does run to completion. My Java code seems to be irrelevant to the problem, so I have not added it (please ask in the comments if you would like it added for further context).

What I Have Tried:

I have tried implementing promises into my code, but I don't think I was doing it properly. My main reference for this was here. I have also looked at the documentation for literally everything related to this topic, however my knowledge of JS is not sufficient to really apply barebones examples to my code.

My Firebase Realtime Database Nodes:

#1: Loop through chat members to collect user IDs:

"Chat Basics" : {
"1607801501690_TQY41wIfArhHDxEisyupZxwyHya2" : {
  "Chat Users" : {
    "JXrclZuu1aOwEpCe6KW8vSDea9h2" : true,
    "TQY41wIfArhHDxEisyupZxwyHya2" : true
  },

#2: Collect user tokens from collected IDs (ignore that tokens are matching):

"User Token Data" : {
"JXrclZuu1aOwEpCe6KW8vSDea9h2" : "duDR3KH3i3I:APA91bH_LCeslZlqL8akYw-LrM9Dv__nx4nU1TquCS0j6bGF1tlIARcheREuNdX1FheC92eelatBC8LO4t6gt8liRdFHV-NDuNLa13oHYxKgl3JBPPlrMo5rB5XhH7viTo4vfYOMftRi",
"TQY41wIfArhHDxEisyupZxwyHya2" : "duDR3KH3i3I:APA91bH_LCeslZlqL8akYw-LrM9Dv__nx4nU1TquCS0j6bGF1tlIARcheREuNdX1FheC92eelatBC8LO4t6gt8liRdFHV-NDuNLa13oHYxKgl3JBPPlrMo5rB5XhH7viTo4vfYOMftRi"

}

Conclusion:

Concrete examples would be much appreciated, especially since I am crunching right now. Thanks for your time and help!

Update:

After some more testing, it looks like the problem is definitely due to my lack of understanding of promises in two areas. Firstly, only one user is collected before the final return is called. Secondly, the final return is called before the 2nd forEach() loop can store snapshot data to an array.

For this code then, how may I modify (or rebuild) it so that it collects all keys before proceeding to retrieve token data from all keys - ultimately before returning the notification?

1
The first order of debugging is to console.log() everything, to make sure values actually are what you think they are.Chris G
You are definitely not dealing with promsies correctly. You're not involving the promise returned by sendToDevice into the final promise that you return from the greater function. That means your messages might not get sent at all. You should also add more logging to determine if you're passing the values you expect to it. The error message is telling you what the problem is.Doug Stevenson
Yes, I agree. I removed all my console.log()s for code clarity - not to say I didn't miss logging something important however.Michael Plischke
@DougStevenson "" passes into sendNotificationPayload(), so it isn't even retrieving it correctly at the first forEach. Would you mind posting an example of what you mean with your promises suggestion?Michael Plischke
There are a lot of resources otu there that help you learn promises. Cloud Functions is by far not the easiest way to learn JavaScript - I suggest following an example built for web.Doug Stevenson

1 Answers

0
votes

Just as with every question I post, I managed to figure out how to do it (tentatively) a few hours later. Below is a full example of how to send a notification to chat users based on a message sent (although it does not yet exclude the sender) to a given chat. The order of operations are as such:

  1. User message is saved and triggers event. Relevant data the message contains are:

    • username, chat key, message

    These are retrieved, with (username + message) as the (title + body) of the notification respectively, and the chat key is used for user id reference.

  2. Loop through chat user keys + collect.

  3. Loop through array of chat user keys to collect array of device tokens.

  4. Send notification when complete.

The code:

//Use firebase functions:log to see log
exports.pushNotification = functions.database.ref('/Chat Messages/{chatId}/{pushId}').onWrite((change, context) => {

    const valueObject = change.after.val(); 

        return admin.database().ref(`/Chat Basics/${valueObject.chatKey}/Chat Users`).once('value', statusSnapshot => {
                
        var index = 0;
        var totalkeys = statusSnapshot.numChildren();
                var msgIDs = [];
        
                statusSnapshot.forEach(msg=>{

                    msgIDs.push(msg.key.toString());
                      
                        if(index === totalkeys - 1){
          
                                const payload = {
                                    notification : {
                                            title: valueObject.userName,
                                body: valueObject.message,
                                sound: "default"
                                    }
                                }

                
                let promises = [];  
                var tokens = [];    
            
                for(let i=0; i < msgIDs.length; i++){
                
                    let userId = msgIDs[i];
                    
                                    let promise = admin.database().ref(`/User Token Data/${userId}`).once('value', snapshot=> {

                        tokens.push(snapshot.val());

                        })
                    promises.push(promise);
                }

                return Promise.all(promises).then(() => {
                    return admin.messaging().sendToDevice(tokens, payload);
                });

                        }
        
                        index++;

            return false;

            });

        });

});