FCM onWrite() function firing twice and sending two notifications? - javascript

I am having issues sending multiple FCM notifications in my iOS app.
I am using Firebase and it's Realtime Database Trigger function, and am able to trigger the Firebase code when a new node is added to the database and when the node is updated.
The problem is that the first time the function runs on write, it runs twice and sends two notifications.
When a node is updated it only runs once and sends only 1 notification, which is the expected behavior.
Below is my javascript code and JSON structure of the database write.
Can anyone please shed any light onto why my function might be fired twice?
var functions = require('firebase-functions');
var admin = require('firebase-admin')
var userDeviceToken = ""
var sharedUserID = ""
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: "https://userlocation-aba20.firebaseio.com/"
});
var payloadStart = {
notification: {
title: "Name of App",
body: "Someone has shared a journey with you."
},
};
var options = {
priority: "high"
}
var payloadEnd = {
notification: {
title: "Name of App",
body: "A shared journey has ended."
},
};
exports.newEntry = functions.database.ref('/StartedJourneys/{fireUserID}')
.onWrite(event => {
const original = event.data.val()
console.log(original.SharedWithUserID)
console.log(original.JourneyEnded)
console.log(event.data.changed())
console.log(event.data.exists())
console.log(event.data.previous)
console.log(event.params)
var payload = payloadStart
if (original.JourneyEnded) {
payload = payloadEnd
}
sharedUserID = original.SharedWithUserID
console.log(sharedUserID)
var db = admin.database()
var ref = db.ref('/UserTokens')
return ref.orderByKey().equalTo(sharedUserID).on("child_added", function(snapshot) {
const deviceToken = snapshot.val()
admin.messaging().sendToDevice(deviceToken, payload, options)
.then(function(response) {
console.log("Successfully sent message:", response);
})
.catch(function(error) {
console.log("Error sending message:", error);
});
})
})
"StartedJourneys" : {
"nFQhaMkjDeSHDAZCklzN7LoGGc22" : {
"CurrentLat" : 37.543821,
"CurrentLong" : -122.239187,
"DestinationLat" : 37.5232217,
"DestinationLong" : -122.2520166,
"DestinationName" : "Nob Hill",
"JourneyEnded" : false,
"SharedWithUser" : "Lisa",
"SharedWithUserID" : "mSJoMJPWWBZEnbq8X05BHwrSd2M2"
}
},
EDIT: I have reduced the scope of the question in hope of getting a response. Thanks in advance!
**EDIT: Added screenshot of the 2 console logs triggered by the function. I should only be seeing one of these.
Firebase logs

Related

Advice converting an onCreate firebase cloud function trigger with FCM Messaging to support async/await and database reads

I initially had a simple firebase cloud function that sent out a push notification to a topic when a new message child was created in my real-time database. But I wanted to add message filtering where notifications for messages from some filtered users would be sent only to admin users. For this, I have created user groups in my real-time database of the format {userName: FIRToken}, which gets written to from my iOS App every time it launches and I get a FIRToken. So now I will have to load 2 lists 1) Admin Users, 2) Filtered Users before I can actually decide where to send the notification.
So I looked into ways to do this and async/await seemed better than doing a promise inside a promise for loading my 2 user lists. I then saw a firestore video tutorial where a similar usecase function was converted to use async/await instead of promises in promises. Following that, I refactored my code to await on the 2 snapshots for admin and filtered users, before going on to decide where to send the notification and return a promise. My refactoring seems correct. But unfortunately, my old iPhone is stuck on <DeviceName> is busy: Copying cache files from device. Hence I can't physically login from 2 different devices and test if the notifications are going only to my admin user account. Which is why I am posting my function here to see if I have refactored my code correctly or missed something. Please let me know if I will get the intended results or I should fix something in the code.
Edit: Updated code to fix these issues:
Also, the methods to send messages are very confusing. send needs topic name to be defined in the payload but does not support apns. sendToTopic needs a topic name as an argument with the payload. sendMulticast fails to send messages to users whereas sendToDevice sends properly.
Finally sendToDevice supports sound field in notification field, but send does not.
functions.database
.ref("/discussionMessages/{autoId}/")
.onCreate(async (snapshot, context) => {
// console.log("Snapshot: ", snapshot);
try {
const groupsRef = admin.database().ref("people/groups");
const adminUsersRef = groupsRef.child("admin");
const filteredUsersRef = groupsRef.child("filtered");
const filteredUsersSnapshot = await filteredUsersRef.once("value");
const adminUsersSnapshot = await adminUsersRef.once("value");
var adminUsersFIRTokens = {};
var filteredUsersFIRTokens = {};
if (filteredUsersSnapshot.exists()) {
filteredUsersFIRTokens = filteredUsersSnapshot.val();
}
if (adminUsersSnapshot.exists()) {
adminUsersFIRTokens = adminUsersSnapshot.val();
}
// console.log(
// "Admin and Filtered Users: ",
// adminUsersFIRTokens,
// " ",
// filteredUsersFIRTokens
// );
const topicName = "SpeechDrillDiscussions";
const message = snapshot.val();
// console.log("Received new message: ", message);
const senderName = message.userName;
const senderCountry = message.userCountryEmoji;
const title = senderName + " " + senderCountry;
const messageText = message.message;
const messageTimestamp = message.messageTimestamp.toString();
const messageID = message.hasOwnProperty("messageID")
? message.messageID
: undefined;
const senderEmailId = message.userEmailAddress;
const senderUserName = getUserNameFromEmail(senderEmailId);
const isSenderFiltered = filteredUsersFIRTokens.hasOwnProperty(
senderUserName
);
console.log(
"Will attempt to send notification for message with message id: ",
messageID
);
var payload = {
notification: {
title: title,
body: messageText,
},
data: {
messageID: messageID,
messageTimestamp: messageTimestamp,
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
};
console.log("Is sender filtered? ", isSenderFiltered);
if (isSenderFiltered) {
adminFIRTokens = Object.values(adminUsersFIRTokens);
console.log("Sending filtered notification with sendMulticast()");
payload.tokens = adminFIRTokens; //Needed for sendMulticast
return admin
.messaging()
.sendMulticast(payload)
.then((response) => {
console.log(
"Sent filtered message (using sendMulticast) notification: ",
JSON.stringify(response)
);
if (response.failureCount > 0) {
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(adminFIRTokens[idx]);
}
});
console.log(
"List of tokens that caused failures: " + failedTokens
);
}
return true;
});
} else {
console.log("Sending topic message with send()");
payload.topic = topicName;
return admin
.messaging()
.send(payload)
.then((response) => {
console.log(
"Sent topic message (using send) notification: ",
JSON.stringify(response)
);
return true;
});
}
} catch (error) {
console.log("Notification sent failed:", error);
return false;
}
});

How to send notification using FCM?

I wrote a cloud function, to listen for document creation in a collection, in my database
here is the function,
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().functions);
var newData;
exports.myTrigger = functions.firestore.document('FCM/{id}').onCreate(async (snapshot, context) => {
//
if (snapshot.empty) {
console.log('No Devices');
return;
}
newData = 'hello';
const deviceIdTokens = await admin
.firestore()
.collection('FCM')
.get();
var tokens = [];
var i=0;
for (var token of deviceIdTokens.docs) {
tokens.push(token.data().ar1[i]);
i++;
}
var payload = {
notification: {
title: 'push title',
body: 'push body',
sound: 'default',
},
data: {
push_key: 'Push Key Value',
key1: newData,
},
};
try {
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
} catch (err) {
console.log(err);
}
});
This function works weirdly,
For example, sometimes it sends notification, and sometimes it does not.
It throws errors like " TypeError: Cannot read property '0' of undefined".
I don't know how to resolve this issue,
In my arr1 field, i have an array of device tokens, to whom i want to send notifications to,
i want the function to send notifications only to the devices(using tokens) which are just created(in the newly created document ),then delete the document.
I think it's sending notifications to all the documents at once.
I'm pretty new at node..
please help me out.
UPDATE:-
Here is my document structure
Type error coming from this line:
tokens.push(token.data().arr1[i]);
So all I can say is that sometimes token.data() doesn't have an arr1 attribute.

Notification not being sent via firebase functions. "undefined" logged in console

I am trying to send a sample notification to all devices according to their token, however the token is being logged as "undefined" and the notification subsequently fails to deliver
The following lines from my code successfully show me the data from the database:
const notificationSnapshot = change.after.val(); //get new value
console.info(notificationSnapshot);
However, the following gives "undefined", despite the above retrieving the data successfully.
const userToken = notificationSnapshot.token;
console.info(userToken);
Is this not the correct way to retrieve the token to send the notification to all the registered devices in my firebase database?
my whole function (index.js)
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp();
exports.sendSampleNotification = functions.database.ref('/User')
.onWrite((change, context) => {
const notificationSnapshot = change.after.val(); //get new value
const userToken = notificationSnapshot.token;
const name = notificationSnapshot.name;
const surname = notificationSnapshot.surname;
console.info(notificationSnapshot);
console.info(userToken);
var message = {
notification: {
title: 'test title',
body: 'test message'
},
token: userToken
};
admin.messaging().send(message).then((response) => {
console.log("Message sent successfully:", response);
return response;
})
.catch((error) => {
console.log("Error sending message: ", error);
});
});
I would say that your issue is very similar to this one since you are having a missing token (showed as undefined) due to the executions times, more or less what Doug was pointing out.
Note that the solution relies on considering the execution times and I’ve seen also that the implementation differs in some method executions but I would say the generals point in the same direction.

Send messages to specific devices using firebase functions

I tried sending a specific message to a client device using node js and firebase functions. But when I tried executing the function, it came back with an error saying:
Error. Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array.
The image is shown below.
I was guessing it's from my JS code. So I am posting that too. What I am actually do is retrieving a data from a specific node to be used when a totally different node is being written. So I am gonna post the JS code before the database screenshots.
exports.sendNotification8 = functions.database.ref('/Users/{user_id}/Notifications/')
.onWrite(( change,context) =>{
var user_id = context.params.user_id;
// Grab the current value of what was written to the Realtime Database.
var eventSnapshot = change.after.val();
var device_token = admin.database().ref('/Users/{user_id}/device_token').once('value');
return device_token.then(result => {
var token_id = result.val();
var str = eventSnapshot.from + " : " + eventSnapshot.message;
console.log(eventSnapshot.from);
var payload = {
data: {
name: str,
title: eventSnapshot.from,
click_action: "Chats"
}
};
// Send a message to devices subscribed to the provided topic.
return admin.messaging().sendToDevice(token_id, payload).then(function (response) {
// See the MessagingTopicResponse reference documentation for the
// contents of response.
console.log("Successfully sent message:", response);
return;
})
.catch(function (error) {
console.log("Error sending message:", error);
});
});
});
And below is my database screenshots...
So that's how I am retrieving the device_token node. From the user that had the newest data written to his/her notifications node. Please help. What am I doing wrong?
Wow. This has been torture. But it finally worked. I got something like this.
exports.sendNotification8 = functions.database.ref('/Users/{user_id}/Notifications/{notifications_id}')
.onWrite((change,context) =>{
var user_id = context.params.user_id;
console.log(user_id);
// Grab the current value of what was written to the Realtime Database.
var eventSnapshot = change.after.val();
var device_token = admin.database().ref('/Users/'+user_id+'/device_token').once('value');
return device_token.then(result => {
var token_id = result.val();
console.log(token_id);
var str = eventSnapshot.message;
console.log(eventSnapshot.from);
var payload = {
data: {
name: str,
title: eventSnapshot.from,
click_action: "Chats"
}
};
// Send a message to devices subscribed to the provided topic.
return admin.messaging().sendToDevice(token_id, payload).then(function (response) {
// See the MessagingTopicResponse reference documentation for the
// contents of response.
console.log("Successfully sent message:", response);
return;
})
.catch(function (error) {
console.log("Error sending message:", error);
});
});
});

How can I send a notification by using the email inserted by another user who requested the notification using Cloud Functions for Firebase?

I'm a beginner on Cloud Functions for Firebase and I'm developing a web app using it to send notifications to specific users. However, the problem is that I want to let the user decid for who he wants to send the message, and the way I've found out to do this was allowing the user to insert through a form in my site the receiver's email, so that I could save it in my database and then activate a function that would send a previously created notification to the registered user who had the same email inserted by the sender user.
So, I know that I have to trigger the function whenever a user sends the form with the receiver's email, since it's when my database is changed. However, I don't know how to compare the email inserted with the email of all others users and then catch only the right user's token to send the notification. Does anyone know how to do this?
These are my code, which I took some parts of this question as a base, and the JSON from part of my database:
Function
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendPush =
functions.database.ref('/Messages/Receivers/{pushId}').onWrite(event => {
const snapshot = event.data;
const email = snapshot.val().email;
const getAllUsersPromise = admin.database().ref('Users/').once('value');
const payload = {
notification: {
title: 'You have a notification',
body: 'You received a new message'
}
};
return getAllUsersPromise.then(result => {
const userUidSnapShot = result;
const users = Object.keys(userUidSnapShot.val());
var AllUsersFCMPromises = [];
for (var i = 0;i<userUidSnapShot.numChildren(); i++) {
const user=users[i];
console.log('getting promise of user uid=',user);
AllUsersFCMPromises[i]= admin.database().ref(`/Users/${user}/email`).equalTo(email).once('value').then(token => {
var token = admin.database().ref(`/Users/${user}/token`).once('value');
return token;
});
}
return Promise.all(AllUsersFCMPromises).then(results => {
var tokens = [];
for(var i in results){
var usersTokenSnapShot=results[i];
console.log('For user = ',i);
if(usersTokenSnapShot.exists()){
if (usersTokenSnapShot.hasChildren()) {
const t= Object.keys(usersTokenSnapShot.val());
tokens = tokens.concat(t);
console.log('token[s] of user = ',t);
}
else{
}
}
}
console.log('final tokens = ',tokens," notification= ",payload);
return admin.messaging().sendToDevice(tokens, payload).then(response => {
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to uid=', tokens[index], error);
if (error.code === 'messaging/invalid-registration-token' || error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(usersTokenSnapShot.ref.child(tokens[index]).remove());
}
}
else{
console.log("notification sent",result);
}
});
});
});
});
});
JSON Structure
{
"Messages" : {
"Receivers" : {
"-Ko-Gc8Ch58uYKGIT_Ze" : {
"email" : "phgrespan#gmail.com"
},
}
},
"Users" : {
"1rwdq0O9Iqdo1JUNauwmlC9HXfY2" : {
"apartamento" : "12",
"bloco" : "L",
"celular" : "148162491784",
"email" : "jose#gmail.com",
"nome" : "josé",
"sobrenome" : "josé",
"telefone" : "418947912497",
"token" : "een1HZ0ZzP0:APA91bHaY06dT68W3jtlC3xykcnnC7nS3zaNiKBYOayBq-wuZsbv1DMFL8zE6oghieYkvvSn39bDCkXLtc3pfC82AGd8-uvmkuXCkPoTuzxMk14wVQNOB01AQ6L7bmsQBisycm2-znz7"
},
"CZv1oxUkfsVpbXNBMQsUZNzSvbt1" : {
"apartamento" : "8",
"bloco" : "P",
"celular" : "123456789",
"email" : "phgrespan#gmail.com",
"nome" : "Pedro",
"sobrenome" : "Henrique",
"telefone" : "99876543234",
"token" : "dvE4gBh1fwU:APA91bF9zLC1cOxT4jLsfPRdsxE8q0Z9P4uKuZlp8M5fIoxWd2MOS30u4TLuOQ4G2Sg0mlDqFMuzvjX3_ZSSi9XATyGtTtNse4AxwLYuD-Piw9oFn6Ma68nGfPSTnIEpvDYRwVnRI2e4"
},
}
}
I hope I have been able to make myself understood and thanks since then.
It seems like you're querying all users when you only need to query the user or users who have the selected email. Instead of using getAllUsersPromise, use .orderByChild() and query the children whose email is equal to the selected email.
let selectedUsers = admin.database.ref('Users/').orderByChild('email').equalTo(email).once(snap => {
// Get the token
})
This will give you the snapshot of just the user or users who have that email. You can then iterate through the snapshots and get the token(s).

Categories