I'm deploying a mobile application (for Android and iOS) through which the admin can send alert to users registered to a specific topic. To do that I'm using Realtime Database to store alerts and cloud functions to send notifications to topic.
I've the following cloud function deployed:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNewAlertNotification = functions.database.ref('/alerts').onWrite(event => {
const getValuePromise = admin.database()
.ref('alerts')
.orderByKey()
.limitToLast(1)
.once('value');
return getValuePromise.then(snapshot => {
const { text, topics, author } = snapshotToArray(snapshot)[0];
const payload = {
data: {
title: 'Avviso',
body: text,
icon: 'ic_stat_notify',
sound: 'default',
color: '#F3E03B',
tag: 'alerts',
ticker: 'Nuovo avviso',
subtitle: 'Avvisi',
author: JSON.stringify(author)
}
};
const options = {
priority: 'high',
timeToLive: 60 * 60 * 24 * 2, // 48 hours
collapseKey: 'it.bmsoftware.caliup'
// contentAvailable: true
};
if (topics.length > 1) {
let condition = '';
topics.forEach((topic, index) => {
condition += `'${topic}' in topics`
if (index < topics.length - 1) {
condition += ' || '
}
});
console.log(`Sending alert to condition '${condition}' -> ${JSON.stringify(payload)}`);
return admin.messaging().sendToCondition(condition, payload, options);
} else if (topics.length === 1) {
let topic = topics[0];
console.log(`Sending alert to topic '${topic}' -> ${JSON.stringify(payload)}`);
return admin.messaging().sendToTopic(topic, payload, options);
} else {
console.log(`No topics found`);
}
});
});
const snapshotToArray = (snapshot) => {
let result = []
if (!snapshot || !snapshot.val())
return result
snapshot.forEach((childSnapshot) => {
let item = childSnapshot.val()
item.key = childSnapshot.key
result.push(item)
})
return result
}
When I insert a new message on the realtime database, the above function fetch that message correctly and in the log section (on the firebase console) I see the correct custom log and a log that says status 'ok'.
Despite this, no notification arrives on devices. If I test the same topic from firebase console directly it works fine so devices are properly registered.
Is there something wrong with the cloud function that I'm missing?
I believe that you should uncomment // contentAvailable: true if you are sending only data payload, at least for iOS. That way you'll be able to show and trigger the notification yourself on the app code. If you want the notification to pop up without having to process the data payload, you should pass a notification object on payload.
Notification is limited to these fields tho: https://firebase.google.com/docs/reference/admin/node/admin.messaging.NotificationMessagePayload
Related
I am trying to send a notification to an android device using firebase cloud messaging.
Below is sendNotification which is a cloud function I have deployed on firebase:
const sendNotification = (owner_uid: any, type: any) => {
return new Promise((resolve, reject) => {
admin.firestore().collection('users').doc(owner_uid).get().then((doc) => {
if (doc.exists && doc.data()?.token) {
if (type === 'new_comment') {
console.log('NEW COMMENT');
console.log('TOKEN: ' + doc.data()?.token);
admin.messaging().sendToDevice(doc.data()?.token, {
data: {
title: 'A new comment has been made on your post',
}
}).then((sent) => {
console.log("SENT COUNT " + sent.successCount);
console.log('SENT APPARENTLY')
resolve(sent);
});
}
}
});
});
}
And here is where I'm calling this function:
export const updateCommentsCount = functions.firestore.document('comments/{commentId}').onCreate(async (event) => {
const data = event.data();
const postId = data?.post;
const doc = await admin.firestore().collection('posts').doc(postId).get();
if (doc.exists) {
let commentsCount = doc.data()?.commentsCount || 0;
commentsCount++;
await admin.firestore().collection('posts').doc(postId).update({
'commentsCount': commentsCount
})
return sendNotification(doc.data()?.owner, 'new_comment');
} else {
return false;
}
})
However, I'm not receiving a notification on the android device.
And here are the cloud function logs when I leave a comment:
Can someone please tell me why is happening, & how it can be resolved? I can show further code if required.
I managed to find the solution.
In the notification sending method, sendToDevice, I updated the key "data", to "notification" and the notification is now being automatically sent & displayed on the original user's device.
Here is the updated
admin.messaging().sendToDevice(doc.data()?.token, {
notification: {
title: 'A new comment has been made on your post',
}
Bear with me. I've spent a month just PHRASING this question: I've been using Firebase Database and Firebase functions for about a year. I've gotten it to work... but only if I sent the text of the message as a STRING. The problem is that now I wish to receive an OBJECT instead but I'm unsure of how to do this in FireBaseMessage.
My previous structure:
messages
T9Vh5cvUcbqC8IEZowBpJC3
ZWfn7876876ZGJeSNBbCpPmkm1
message
"messages": {
".read": true,
"$receiverUid": {
"$senderUid": {
"$message": {
".read": true,
".write": "auth.uid === $senderUid"
And my function for the listener was this:
exports.sendMessage = functions.database.ref('/messages/{receiverUid}/{senderUid}/{message}')
This is problematic... for a variety of reasons. Namely if the old message was "Hey" and then that same person just writes "Hey" again... then the original gets overwritten.
So my NEW structure is more like this:
messages
-LkVcYqJoEroWpkXZnqr
body: "genius grant"
createdat: 1563915599253
name: "hatemustdie"
receiverUid: "TW8289372984KJjkhdsjkhad"
senderUid: "yBNbs9823789KJkjahsdjkas"
Which is written as:
mDatabase.child("messages").push().setValue(message);
...and I'm just unsure about how to write out that function.
I mean... IDEALLY... it would be something like:
exports.sendMessage = functions.database.ref('/messages/{receiverUid}/{senderUid}/{msgID}/{msgOBJECT}')
...but I'm just not sure how Firebase functions is reading this new structure.
Now I'm pushing to the database like so:
mDatabase.child("messages").child(guid).child(user_Id).push().setValue(msgObject).addOnSuccessListener(this, new OnSuccessListener<Void>() {
#Override
public void onSuccess(#NonNull Void T) {
Log.d("MessageActivity", "Message Sent");
Basically I would just like to receive the message object... with everything in it... when it arrives from the notification... and be able to easily parse the body, date, userids, etc.
Can someone explain the correct way to go about this?
UPATE By request here's the complete cloud function:
exports.sendMessage = functions.database.ref('/messages/{receiverUid}/{senderUid}/{msgId}/{message}')
.onWrite(async (change, context) => {
const message = context.params.message;
// const messageId = context.params.messageId;
const receiverUid = context.params.receiverUid;
const senderUid = context.params.senderUid;
// If un-follow we exit the function.
if (!change.after.val()) {
return console.log('Sender ', senderUid, 'receiver ', receiverUid, 'message ', message);
}
console.log('We have a new message: ', message, 'for: ', receiverUid);
// Get the list of device notification tokens.
const getDeviceTokensPromise = admin.database()
.ref(`/users/${receiverUid}/notificationTokens`).once('value');
// Get the follower profile.
const getSenderProfilePromise = admin.auth().getUser(senderUid);
// The snapshot to the user's tokens.
let tokensSnapshot;
// The array containing all the user's tokens.
let tokens;
const results = await Promise.all([getDeviceTokensPromise, getSenderProfilePromise]);
tokensSnapshot = results[0];
const sender = 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 sender profile', sender);
// console.log('David you're looking for the following UID:', followerUid);
// Notification details.
const payload = {
notification: {
title: `${sender.displayName} sent you a message.`,
body: message,
tag: senderUid
},
// 'data': { 'fuid': followerUid }
data: {
type: 'message',
name: sender.displayName
}
};
console.log('David you are looking for the following message:', message);
// Listing all tokens as an array.
tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
const response = await admin.messaging().sendToDevice(tokens, payload);
// 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);
});
Since you now store the sender and receiver's UIDs inside the message, the declaration of your Cloud Function will need to change.
Instead of this:
exports.sendMessage = functions.database.ref('/messages/{receiverUid}/{senderUid}/{msgId}/{message}').onWrite(async (change, context) => {
You'll need to trigger on:
exports.sendMessage = functions.database.ref('/messages/{messageId}').onWrite(async (change, context) => {
So with this change your code will trigger on each message that is written /messages.
Now you "just" need to get the sender and receiver's UID. And since you no longer can get them from the context, you will instead get them from the change. Specifically change.after contains the data snapshot as it exists in the database after the write has completed. So (as long as you're not deleting the data), you can get the UIDs with:
const receiverUid = change.after.val().receiverUid;
const senderUid = change.after.val().senderUid;
And you'll also get the actual message from there of course:
const message = change.after.val().message;
And just in case you need the message ID (the -L... key that it was written under in the database):
const messageId = change.after.val().messageId;
You need a trigger on just the messageId:
exports.sendMessage = functions.database.ref('/messages/{messageId}').onWrite((change, context) => {
const changedData = change.after.val(); // This will have the complete changed data
const message = change.after.val().message; // This will contain the message value
......
});
Elaborating on Frank's answer:
You can't get the data from context like const message = context.params.message;because those parameters don't exists anymore on the context.
I have a firebase cloud function that is supposed to send a notification to the 5 closest phones when a new "emergency" is added to the database. The code I wrote does send a notification to the 5 closest phones, but It sends that same notification over and over again. It gets even worse when my users log on or off because then it sends even more. I'm confused why my cloud function doesn't just operate once and then terminate.
Here is the code for my cloud function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendPushNotificationAdded = functions.database.ref('/emergencies/{id}').onCreate(event => {
return admin.database().ref('/tokens').on('value', function(snapshot) {
var efarArray = snapshotToArray(snapshot, event.data.child('latitude').val(), event.data.child('longitude').val());
efarArray.sort(function(a, b) {
return a.distance - b.distance;
});
var payload = {
notification: {
title: "NEW EMERGANCY!",
body: "Message from Patient: " + event.data.child('other_info').val(),
//badge: '1',
sound: 'default',
}
};
var options = {
priority: "high",
timeToLive: 0
};
tokens_to_send_to = [];
if(efarArray.length >= 5){
//only send to the 5 closest efars
for (var i = 4; i >= 0; i--) {
tokens_to_send_to.push(efarArray[i].token);
}
}else{
for (var i = efarArray.length - 1; i >= 0; i--) {
tokens_to_send_to.push(efarArray[i].token);
}
}
//TODO: send a messaged back to patient if no efars respond or are found?
return admin.messaging().sendToDevice(tokens_to_send_to, payload, options).then(response => {
});
});
});
//code for function below from https://ilikekillnerds.com/2017/05/convert-firebase-database-snapshotcollection-array-javascript/
function snapshotToArray(snapshot, incoming_latitude, incoming_longitude) {
var returnArr = [];
snapshot.forEach(function(childSnapshot) {
var distance_to_efar = distance(childSnapshot.child('latitude').val(), childSnapshot.child('longitude').val(), incoming_latitude, incoming_longitude);
var item = {
latitude: childSnapshot.child('latitude').val(),
longitude: childSnapshot.child('longitude').val(),
token: childSnapshot.key,
distance: distance_to_efar
};
returnArr.push(item);
});
return returnArr;
};
If more clarification or code is needed just let me know. I've been stuck on this forever...
Don't use on() with Cloud Functions. That's almost never the right thing to use, since it adds a listener that could be invoked any number of times as the database changes. Use once() to get a single snapshot and act on that.
Also, you must return a promise from the function that resolves when all the asynchronous work in that function is complete. on() doesn't return a promise, so your function isn't doing that as well.
You might want to study some of the official sample code and follow the patterns established there.
I want to display a group notification instead of multiple notifications like whatsapp does.
For eg:
One notification with message - "2 discussions 1 comment" instead of
total three notifications.
I used react-native-fcm library (https://github.com/evollu/react-native-fcm)
I used group & tag keys but couldn't achieve the result as below code
FCM.presentLocalNotification({
title: 'Title',
body: 'Body',
priority: "high",
click_action: true,
show_in_foreground: true,
local: true,
group: 'group1',
tag: 'tag1'
});
Is it possible to achieve this functionality in react native FCM? Please let me know.
The project react-native-fcm is moved under react-native-firebase and there is a solution under this issue on the project.
The main idea:
The trick is to create an additional notification that will contain the notifications for that group.
// ID for grouping notifications, always the same
const SUMMARY_ID = `${ALERTS_GROUP}.summary`
const sendIt = (notification: Firebase.notifications.Notification) => {
return firebase.messaging().hasPermission().then((yes) => {
if (yes) {
try {
return firebase.notifications().displayNotification(notification)
.catch((err) => {
Log.e(`[sendNotification] ERROR: ${err}`)
return Promise.resolve()
})
} catch (err) {
Log.e('[sendNotification] Error displaying notification: ' + err)
}
}
return Promise.resolve()
})
}
const sendSummary = (data: MessageData) => {
const summary = new firebase.notifications.Notification()
.setNotificationId(SUMMARY_ID)
.setTitle(_T('notification.channels.alert.description'))
.setData(data)
.android.setAutoCancel(true)
.android.setCategory(firebase.notifications.Android.Category.Message)
.android.setChannelId(getChannelId(MsgType.Alert))
.android.setColor(variables.scheme.primaryColor)
.android.setSmallIcon(STATUS_ICON)
.android.setGroup(ALERTS_GROUP)
.android.setGroupSummary(true)
.android.setGroupAlertBehaviour(firebase.notifications.Android.GroupAlert.Children)
sendIt(summary)
}
/**
* Called by `bgMessaging` or the `onMessage` handler.
*/
export function sendNotification (message: Firebase.messaging.RemoteMessage) {
const payload: MessagePayload = message.data as any || {}
const notification = new firebase.notifications.Notification()
// ... more code
if (Platform.OS === 'android' && Platform.Version >= 24) {
notification.android.setGroup(ALERTS_GROUP)
sendSummary(notification.data)
}
Log.v('[sendSummary] sending notification.')
return sendIt(notification)
}
I am trying to make a cloud function that sends a push notification to a given user.
The user makes some changes and the data is added/updated under a node in firebase database (The node represents an user id). Here i want to trigger a function that sends a push notification to the user.
I have the following structure for the users in DB.
Users
- UID
- - email
- - token
- UID
- - email
- - token
Until now i have this function:
exports.sendNewTripNotification = functions.database.ref('/{uid}/shared_trips/').onWrite(event=>{
const uuid = event.params.uid;
console.log('User to send notification', uuid);
var ref = admin.database().ref('Users/{uuid}');
ref.on("value", function(snapshot){
console.log("Val = " + snapshot.val());
},
function (errorObject) {
console.log("The read failed: " + errorObject.code);
});
When i get the callback, the snapshot.val() returns null. Any idea how to solve this? And maybe how to send the push notification afterwards?
I managed to make this work. Here is the code that sends a notification using Cloud Functions that worked for me.
exports.sendNewTripNotification = functions.database.ref('/{uid}/shared_trips/').onWrite(event=>{
const uuid = event.params.uid;
console.log('User to send notification', uuid);
var ref = admin.database().ref(`Users/${uuid}/token`);
return ref.once("value", function(snapshot){
const payload = {
notification: {
title: 'You have been invited to a trip.',
body: 'Tap here to check it out!'
}
};
admin.messaging().sendToDevice(snapshot.val(), payload)
}, function (errorObject) {
console.log("The read failed: " + errorObject.code);
});
})
Just answering the question from Jerin A Mathews...
Send message using Topics:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
//Now we're going to create a function that listens to when a 'Notifications' node changes and send a notificcation
//to all devices subscribed to a topic
exports.sendNotification = functions.database.ref("Notifications/{uid}")
.onWrite(event => {
//This will be the notification model that we push to firebase
var request = event.data.val();
var payload = {
data:{
username: request.username,
imageUrl: request.imageUrl,
email: request.email,
uid: request.uid,
text: request.text
}
};
//The topic variable can be anything from a username, to a uid
//I find this approach much better than using the refresh token
//as you can subscribe to someone's phone number, username, or some other unique identifier
//to communicate between
//Now let's move onto the code, but before that, let's push this to firebase
admin.messaging().sendToTopic(request.topic, payload)
.then((response) => {
console.log("Successfully sent message: ", response);
return true;
})
.catch((error) => {
console.log("Error sending message: ", error);
return false;
})
})
//And this is it for building notifications to multiple devices from or to one.
Return this function call.
return ref.on("value", function(snapshot){
console.log("Val = " + snapshot.val());
},
function (errorObject) {
console.log("The read failed: " + errorObject.code);
});
This will keep the cloud function alive until the request is complete. Learn more about returning promises form the link give by Doug in the comment.
Send Notification for a Topic In Cloud function
Topics a basically groups you can send notification for the selected group
var topic = 'NOTIFICATION_TOPIC';
const payload = {
notification: {
title: 'Send through Topic',
body: 'Tap here to check it out!'
}
};
admin.messaging().sendToTopic(topic,payload);
You can register the device for any new or existing topic from mobile side
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotificationToTopic =
functions.firestore.document('Users/{uuid}').onWrite(async (event) => {
//let title = event.after.get('item_name');
//let content = event.after.get('cust_name');
var message = {
notification: {
title: "TGC - New Order Recieved",
body: "A New Order Recieved on TGC App",
},
topic: 'orders_comming',
};
let response = await admin.messaging().send(message);
console.log(response);
});
For sending notifications to a topic, The above code works well for me, if you have any doubt, let me know.