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.
Related
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
I'm trying to implement a messaging application using Firebase Firestore and Firebase Cloud Functions.
In essence, chat messages are stored as individual documents in a subcollection. At first, I implemented this as directly adding a document from the client and listening on the collection and updating the clients when a change happens but later I decided to switch to using Cloud functions so that I can add some functionality that's better done on the server side(filtering etc.).
So I created a function for sending messages, which creates the documents on behalf of the users when the users call the function from the app(i.e. tap the send button).
The function worked and I was able to monitor the processes through the logs. Unfortunately, the functions began to die out without error, the console was reporting that the functions are executed successfully and it usually took less than a second to execute.
I suspect that it has something to do with the promises that probably continue to run but this is the same code that was working but failing today.
If I try a few more times, the functions seem to be working again. Do I need to keep the functions "warm"? Are cloud functions not reliable enough to handle this kind of tasks? When I say my user that a message is sent, I need to be able to confirm that it is sent and communicate it with the users if it failed.
It's hard to debug the issue because no errors are thrown(not even info message, it's just as if didn't happen), it just says that the function successfully finished execution and nothing happened.
Am I missing something here? Thank you.
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
return;
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent")
}).catch(err => {
console.log("Error sending mesage:", err)
})
})
As explained in the documentation of the HTTP Callable Cloud Functions:
To return data after an asynchronous operation, return a promise.
Then follows an example:
const sanitizedMessage = sanitizer.sanitizeText(text); // Sanitize the message.
return admin.database().ref('/messages').push({
text: sanitizedMessage,
author: { uid, name, picture, email },
}).then(() => {
console.log('New Message written');
// Returning the sanitized message to the client.
return { text: sanitizedMessage };
})
So you need to adapt your code as follows:
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
//Here send back an error as explained here: https://firebase.google.com/docs/functions/callable#handle_errors
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
//Note the return on next line
return admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent");
return { text: "Message sent" };
}).catch(err => {
console.log("Error sending mesage:", err);
//Here, again, send back an error as explained here: https://firebase.google.com/docs/functions/callable#handle_errors
})
})
If you don't want to return a value to the client, you could do as follows, returning null when the Promise returned by the add() asynchronous method resolves. (Not tested but it should work).
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
return null;
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
//Note the return on next line
return admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent"); //Actually, if you don't need this console.log() you can remove this entire then() block, returning the promise from add() is enough
return null;
}).catch(err => {
console.log("Error sending mesage:", err);
return null;
})
})
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.
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');
});
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.