Cloud Functions for Firebase send push to multiple users - firebase

I try to get multiple push notifications to run on Cloud Functions for Firebase without success.
I store my message receipts in a node
message_receipts
-KtvyTN3nbVKoFjHdpJg
hHhs5Aco38X1W8EhaaxrwsQDXwy1:"receipt"
nI25FjUnBfQiCWzdCUIAe8CWTPQ2:"receipt"
To send push notifications I try following on cloud functions:
//*********************************************************************************************************** */
//handle lsit item added by shared user
if (String(msgData.messageType) == 'ListItemAddedBySharedUser') {
return admin.database().ref("message_receipts").child(event.params.messageID).once('value').then(receipts => {
receipts.forEach(function (receipt) {
//Send push to receipt
return admin.database().ref('/users/' + receipt.key).once('value').then(usnap => {
//Send push to users fcmToken
const userSnap = usnap.val()
console.log('sending Push to ' + userSnap.fcmToken)
//create Notification Payload
var payload = {
notification: {
title: msgData.title,
body: msgData.message,
badge: '1',
sound: 'default',
sbID: String(event.data.key),
senderID: msgData.senderID,
listID: msgData.listID,
receiptID: receipt.key,
notificationType: String(msgData.messageType),
}
};
return admin.messaging().sendToDevice(userSnap.fcmToken, payload).then(response => {
console.log("Successfully sent invite message:", response)
console.log(response.results[0].error)
}).catch((err) => { console.log("Error sending Push", err) })
})
})
})
} //*********************************************************************************************************** */
All I get is one notification sent.
I'am very new to java script and cloud functions.
What do I need to do to get all of my users notified?

You need to aggregate all of the asynchronous actions you're taking. Here you're doing a forEach on the message receipts, but then you're returning a single promise. Try something like:
var promises = [];
receipts.forEach(function (receipt) {
//Send push to receipt
promises.push(admin.database().ref('/users/' + receipt.key).once('value').then(usnap => {
/* ... */
}))
})
return Promise.all(promises);
This will aggregate all of your outstanding notifications into a single Promise.all call, which will wait until they all complete.

You can send a batch of messages as described in the documentation:
https://firebase.google.com/docs/cloud-messaging/send-message#send-a-batch-of-messages
// Create a list containing up to 500 messages.
const messages = [];
messages.push({
notification: {title: 'Price drop', body: '5% off all electronics'},
token: registrationToken,
});
messages.push({
notification: {title: 'Price drop', body: '2% off all books'},
topic: 'readers-club',
});
return admin.messaging().sendAll(messages)
.then((response) => {
console.log(response.successCount + ' messages were sent successfully');
});

Related

Push Notifications for follow system fails after fetching FCM tokens from firestore

I'm writing a cloud function for my iOS app to watch for any follower changes in my users to notify them when someone follows them. My follower subcollection is within each user data document and I use wildcards to listen to any changes. I've also provided good logging during each step, so it's easy to see where the problem is, however, since I'm rather new to cloud functions, I don't know exactly how I'd fix it.
The cloud function is as follows.
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
let title
let body
let payload
let FCMRegistrationToken_KEY
exports.sendNotificationOnFollowerCreate = functions.firestore
.document('Users/{userID}/Followers/{followerID}')
.onCreate((snapshot, context) => {
if(snapshot.after){
// Get the userId and followerId
const userID = context.params.userID;
const followerID = context.params.followerID;
// Get the data of the follower document
const newData = snapshot.after.data()
const fullName = newData.firstName + " " + newData.lastName
title = 'Someone just followed you'
body = fullName + ' Just followed you right now!\n' + 'username: ' + newData.userName
// Create the notification payload
payload = {
notification: {
title: title,
body: body
}
}
// Get FMC token by fetching the FCMToken Document for the userID above.
admin.firestore().collection('FCMTokens').doc(userID).get().then(doc => {
if(!doc.exists) {
console.log('User not found!');
} else {
// Get the data of the document
const data = doc.data();
console.log(data);
FCMRegistrationToken_KEY = data.token
}
})
.catch(error => {
console.log(error);
})
.finally(() => {
//more code here
// Send the notification
admin.messaging().sendToDevice(FCMRegistrationToken_KEY, payload)
.then(response => {
console.log('Notification sent successfully:', response);
})
.catch(error => {
console.log('Error sending notification:', error);
});
});
}
})
Basically when there's a new follower added, I use the userID from the context parameters to fetch the FCM token I have saved for all my users in a FCMTokens collection. After retrieving the token and creating my payload, I invoke a sendToDevice() call through admin.messaging() but it fails for some reason.
However, it fails right after that giving the following error
{
"textPayload": "Function returned undefined, expected Promise or value",
"insertId": "63c38ba0000e2c35c9c62c1d",
"resource": {
"type": "cloud_function",
"labels": {
"function_name": "sendNotificationOnFollowerCreate",
"region": "us-central1",
"project_id": "fir-eris"
}
},
"timestamp": "2023-01-15T05:14:08.928821Z",
"severity": "WARNING",
"labels": {
"execution_id": "no23uq1mg5a3",
"instance_id": "00c61b117c173e48fc2cb6c3b49f2c059090e49b7252db1b187115bd42a62998c4093f283fe06ba4ec0bf7981f108fcadb527843a8c4b3c77ec1"
},
"logName": "projects/fir-eris/logs/cloudfunctions.googleapis.com%2Fcloud-functions",
"trace": "projects/fir-eris/traces/e0d7dfae3ea1340e1ec101d16defc94b",
"receiveTimestamp": "2023-01-15T05:14:09.204309551Z"
}
I'm thoroughly confused as I really don't have that much experience with cloud functions. Can someone guide me through what's happening and what could be a potential fix for this?
Thank you.
The error that you have mentioned in the question is basically seen when a function does not or has incorrectly a return statement. The code you have for cloud function does not seem to have any return statement which will have a promise return.To make sure Cloud functions knows when your code is done, you need to either return a value from the top-level function (in the case that all work happens synchronously), or return a promise from the top-level function (in the case that some work continues after the closing } of the function).
The sendNotificationOnFollowerCreate might be aborted when the trigger function finishes because it isn't waiting for that promise.
Try adding the return similar to example below:
return DeviceToken.then(result => { const token_id = result.val();
console.log(token_id); const payload = { notification:
{ title: "New Job Request", body: `JobID ` + job_id, tag: collapseKey, } };
return admin.messaging().sendToDevice(token_id, payload)
Also check these following examples with similar implementations:
Each then should return a value firebase cloud function
Send Push notification using Cloud function for firebase
Firebase Cloud function says unreachable
Firebase Cloud push notification not being sent to device
Is there a way to send notification by identifying user rather than
device

Firebase functions cloud messaging notification not being recieved

When I send a notification from the Firebase cloud messaging console, my device receives it without a problem, but when I send it via a cloud functions, the function log says it was successfully sent but my device does not receive it. I tried switching to type script, sending the notification with different conditions but nothing works. The app is written in flutter.
My function code:
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
db.collection("users")
.where("bananas", "==", 1666).get().then(
(result) => {
if (result.size > 0) {
result.forEach((doc) => {
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
fcm.send(payload).then((response) => {
// Response is a message ID string.
console.log("Successfully sent message: "+
doc.data().NotToken+ " ", response);
return {success: true};
}).catch((error) => {
return {error: error.code};
});
});
}
});
response.send("Notification sent !");
functions.logger.info("Notification sent!");
return null;
});
cloud log
Any ideas?
Did you notice how your code never logs this message?
Successfully sent message
That's because both loading from Firestore, and sending messaging through Cloud Messaging are asynchronous calls. So your response.send("Notification sent !") runs before the data is ever retrieved from the database, and Cloud Functions at that point terminates your code to prevent charging after you say that you are done.
If you have asynchronous operations in your code, you need to return a promise from the top-level of your code that resolves/rejects when all asynchronous code has completed. So in your case that means the promise should only resolve once you've loaded the data from Firestore, and sent the messages.
Let's start with a simple example. Say that you want to only send a single message, no matter how many documents are in the database.
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
return db.collection("users") // 👈 Add return here
.where("bananas", "==", 1666).get().then((result) => {
if (result.size > 0) {
const doc = result.docs[0]; // 👈 Get the first result
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
return fcm.send(payload).then((response) => { // 👈 Add return here
console.log("Successfully sent message: "+
doc.data().NotToken+ " ", response);
response.send("Notification sent !"); // 👈 Move this call here
return {success: true};
}).catch((error) => {
// TODO: Send an error back to the caller
return {error: error.code};
});
}
});
});
So the top-level code now returns the result from loading data from Firestore, and in there, we return the call from calling FCM, which then in turn returns return {success: true};. When returning promises, the results bubble up - so you can typically just keep returning the nested results.
You'll also not that we've moved the response.send into the code that runs after calling FCM, as we don't want to send a result back to the caller until the FCM call is done.
The above is the simple variant, because in reality you have multiple documents, and you are only done once all of them are done.
For that we are going to use Promise.all(), which takes an array of promises and resolves once all those promises resolve. So we're going to capture all the calls to FCM (which returns a promise) and collection them in an array, that we then pass to Promise.all().
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
return db.collection("users")
.where("bananas", "==", 1666).get().then((result) => {
if (result.size > 0) {
let promises = [];
result.forEach((doc) => {
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
promises.push(fcm.send(payload))
});
return Promise.al(promises).then((results) => {
console.log("Successfully sent messages");
response.send("Notification sent !");
return {success: true};
});
}
});
});
While this may be a lot to grok all at once, handling asynchronous behavior is quite well covered in the Firebase documentation on terminating functions, in this video series on Learn JavaScript Promises with Cloud Functions, and in quite a few tutorials out there - so I recommend spending some time on those to get to grips with asynchronous code.

How can I write a cloud functions onUpdate Firestore to get values from a collection on update of another collection

I have a collection of orders mapped for a user. Which is in users/{userID}/orders/{orderID}) format. I need a function which onUpdate to this document sends a notification to an array of tokens saved in users/{userID}/tokens
exports.modifyUserCart = functions.firestore
.document('users/{userID}/orders/{orderID}')
.onUpdate((change, context) => {
const document = change.after.exists ? change.after.data() : null;
console.log(document.order_id)
// document.order_id . "This prints correctly"
// The tokens to be added to an array are in (users/{userID}/tokens). How
// do I get the tokens from the collection of tokens
var tokens = [] //array of tokens
var message = {
notification: {
title: 'Get an upfront discount',
body: "Clear your items in cart in the next hour to get an upfront
discount of $100"
},
token: tokens
};
admin
.messaging()
.send(message)
.then((response) => {
// Response is a message ID string.
console.log('Successfully sent message:', response);
})
.catch((error) => {
console.log('Error sending message:', error);
});
// perform desired operations ...
});
I did a firestore database with the same architecture as yours:
users/{userID}/orders/{orderID} and users/{userID}/tokens/{tokenID}
The event I used to trigger the app is:
providers/cloud.firestore/eventTypes/document.update on the resource: users/{userID}/orders/{orderID}
this allowed me that the function executed when there was an update on any document in orders collections.
The sourcecode is in three files:
credentials.json: which is the login info for the service account that will interact with firestore in the second part. to get this file for your own projec you can follow here
package.json:
{
"name": "sample-firestore",
"version": "0.0.1",
"dependencies": {
"#google-cloud/firestore": "^2.6.1",
"firebase-admin": "^8.8.0"
}
}
index.js:
/**
* Triggered by a change to a Firestore document.
*
* #param {!Object} event Event payload.
* #param {!Object} context Metadata for the event.
*/
exports.helloFirestore = (event, context) => {
const resource = context.resource;
// log out the resource string that triggered the function
console.log('Function triggered by change to: ' + resource);
// now log the full event object
console.log(JSON.stringify(event));
console.log("Doc Modified");
const document = event.value.name;
console.log(document);
//ended know which document was modified
//starting GET second collection
const Firestore = require('#google-cloud/firestore');
const db = new Firestore({
projectId: '[YOUR_PROJECT_ID]',
keyFilename: 'credential.json'
});
console.log(JSON.stringify(db));
var str = document.split("/");
console.log(str);
console.log(str[6]);
db.collection('users').doc(str[6]).collection('tokens').get()
.then((snapshot) => {
snapshot.forEach((doc) => {
console.log(doc.id, '=>', doc.data());
});
})
.catch((err) => {
console.log('Error getting documents', err);
});
};
this code in particular only logs the documents it gets to the console log, as an example of getting the documents if you want to perform other operations on the second part here are some code snippets of other operations.

How do I properly grab the users profile from Realtime Database to get their username before the cloud function returns?

I am implementing Cloud Functions to send my users notifications for when interesting things happen like following, liking, commenting. I have copied & adapted the Firebase tutorial for sending a notification when a change at the followers node is detected, but I need to also query the database to get the follower's account data including their username. I think I am close, but the function doesn't finish in time and I'm having trouble understanding promises. Here is the function:
exports.sendFollowerNotification = functions.database.ref(`/userFollowers/{followedUid}/{followerUid}`)
.onWrite((change, context) => {
const followerUid = context.params.followerUid;
const followedUid = context.params.followedUid;
// If un-follow we exit the function
if (!change.after.val()) {
return console.log('User ', followerUid, 'un-followed user', followedUid);
}
console.log('We have a new follower UID:', followerUid, 'for user:', followedUid);
// Get the list of device notification tokens.
const getDeviceTokensPromise = admin.database()
.ref(`/users/${followedUid}/notificationTokens`).once('value');
console.log('Found the followed user\'s token')
const userInfo = admin.database().ref(`/users/${followedUid}`).once('value');
console.log(userInfo)
const username = userInfo['username'];
console.log(username);
////////////////// ABOVE is where I'm trying to get the username by reading their account data ///////////////////
// Get the follower profile.
const getFollowerProfilePromise = admin.auth().getUser(followerUid);
// The snapshot to the user's tokens.
let tokensSnapshot;
// The array containing all the user's tokens.
let tokens;
return Promise.all([getDeviceTokensPromise, getFollowerProfilePromise]).then(results => {
tokensSnapshot = results[0];
const follower = results[1];
// Check if there are any device tokens.
if (!tokensSnapshot.hasChildren()) {
return console.log('There are no notification tokens to send to.');
}
console.log('There are', tokensSnapshot.numChildren(), 'tokens to send notifications to.');
console.log('Fetched follower profile', follower);
// Notification details.
const payload = {
notification: {
title: 'You have a new follower!',
body: `{username} is now following you.`,
}
};
// Listing all tokens as an array.
tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
return admin.messaging().sendToDevice(tokens, payload);
}).then((response) => {
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to', tokens[index], error);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
}
}
});
return Promise.all(tokensToRemove);
});
});
How can I ensure that username will have been made available before it returns? Thanks.
Ok, I think I get what you are saying...
These lines of code don't do what you think. All DB reads are done asynchronous, so...
const userInfo = admin.database().ref(`/users/${followedUid}`).once('value');
console.log(userInfo)
const username = userInfo['username'];
console.log(username);
once returns a promise, so userInfo is actually a promise to return the data. You won't get the data until you do a then.
More chaining promises I'm afraid... just rename userInfo to userInfoPromise and add it to your Promise.All array.

Firebase Cloud Functions called multiple times

Hello my firebase cloud function gets called multiple times when I don't put in check for previous.exists().
I get multiple push notifications.
if (!event.data.exists()){
return;
}
if (event.data.previous.exists()){
return;
}
But when I check for it i don't get push notification.
Here is the not working code:
What should I change?
exports.sendShoppingListInvitationNotification = functions.database.ref('/invites/{id}/').onWrite(event => {
//get the snapshot of the written data
const snapshot = event.data;
if (!event.data.exists()){
return;
}
if (event.data.previous.exists()){
return;
}
//get snapshot values
console.log(snapshot.key);
const receiptToken = snapshot.child('receiptFcmToken').val();
const senderName = snapshot.child('senderNickname').val();
const inviteMessage = snapshot.child('inviteMessage').val();
const senderImage = snapshot.child('senderProfileImageURL').val();
//create Notification
const payload = {
notification: {
title: `Invitation from ${senderName}`,
body: `${inviteMessage}`,
icon: `${senderImage}`,
badge: '1',
sound: 'default',
}
};
//send a notification to firends token
return admin.messaging().sendToDevice(receiptToken, payload).then(response => {
console.log("Successfully sent message:", response);
}).catch((err) => {
console.log(err);
});
});
I don't get error message on cloud console.
This is the firebase structure:
Seems like it shouldn’t be called multiple times unless you’re doing multiple writes to that location. Try using .onCreate instead of .onWriteif you only want to send a notification on the first write to the path. Then you won’t need that check for previous data. See the documentation here which outlines the different database triggers.

Resources