Notifications not showing up on database trigger - firebase

I am using firebase cloud functions for sending notifications to users when database trigger is called.The registration token is saved in firebase database. The problem is that inspite of registration tokens getting saved, the notifications are not showing up in these devices.
This is index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotification = functions.database.ref('/Blog').onWrite(event => {
const title = event.data.child('title').val();
const token_no = event.data.child('token_no').val();
const getDeviceTokensPromise = admin.database().ref(`/Token/${token_no}`).once('value');
const getBody=admin.database().ref(`/Blog`).once('value');
return Promise.all([getDeviceTokensPromise,getBody]).then(results => {
const tokensSnapshot = results[0];
const notify=results[1];
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.');
// Notification details.
const payload = {
notification: {
title: 'You have a new Alert!',
body: `${notify.child('title').val()}`,
}
};
// Listing all tokens.
const 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);
});
});
});
Database snapshot:
"Token" : {
"token_no" : {
"ctxjePemYZE:APA91bFJXXyTkyvXOlSY...4VbWu7Vbf7itwbwZu9umSvg_tdB1lKD1d8g" : "true",
"dgVszInjIW0:APA91bFZE3Av5unZTgp...RUeYb-CUhWjO1otqguhE9NTQZ8XgK6nRIW5" : "true"
}
}

Update 2:
Your code uses Object.keys(tokensSnapshot.val()) to get the tokens. That means the tokens must be the keys under token_no, not the values. Like this:
"Token" : {
"-KoWsMn9rCIitQeNvixr" : {
"dK1FjGbNr6k:APA91b...S8JK2d69JpO" : "123" // token is the key, value is not significant; could be "123", true, or 0.
},
...
}
Update:
You should review the documentation for the Event parameter of a database trigger to get a better understanding of the params and data properties. params provide acces to the wildcards in the trigger reference, data is the snapshot. In your code, you want to get values from the snapshot.
Add these changes:
const title = event.data.child('title').val();
const desp = event.data.child('desp').val();
const token_no = event.data.child('token_no').val()
const payload = {
notification: {
title: 'You have a new Alert!',
body: `${Post.child('title').val()}`, // <= CHANGED
//icon: follower.photoURL
}
};
Running the code you posted, with these changes, I am able to send and receive a notification.
There are two problems. The first is this statement:
const getDeviceTokensPromise = admin.database().ref(`/Token/{token_no}`).once('value');
token_no is not defined. And when you do define it and want to substitute its value, you will need to add a $:
`/Token/${token_no}`
The second problem is that Post is not defined, which causes function execution to fail. Check your function logs:
const payload = {
notification: {
title: 'You have a new Alert!',
body: `${Post.title}`,
//icon: follower.photoURL
}
};

Related

Better code to push notification: firestore - cloud function

I made a cloud function (using google :() that sends push notifications when adding a document in firestore, but I have the error that you see in the image and the notifications do not arrive but I do not understand what may be wrong in my code, can someone help me?
mi code:
exports.cambiaColeccion = functions.firestore
.document('sendMessage/{docId}')
.onCreate((snap, context) => {
const nuevoMensaje= snap.data();
console.log('id', nuevoMensaje);
console.log('titulo', nuevoMensaje.titulo)
enviaMensage();
});
async function enviaMensage() {
console.log('en enviaMensaje');
const payload ={
notification: {
title: "Titulo del mensaje",
body: "Texto del mensaje ... ",
sound: 'default',
badge: '1',
}
// Get the list of device tokens.
const allTokens = await admin.firestore().collection('FCMTokens').get();
const tokens = [];
allTokens.forEach((tokenDoc) => {
tokens.push(tokenDoc.id);
});
if (tokens.length > 0) {
// Send notifications to all tokens.
const response = await admin.messaging().sendToDevice(tokens, payload);
//await cleanupTokens(response, tokens);
console.log('Notifications have been sent and tokens cleaned up.');
}
return true
}
// Cleans up the tokens that are no longer valid.
function cleanupTokens(response, tokens) {
// For each notification we check if there was an error.
const tokensDelete = [];
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') {
const deleteTask = admin.firestore().collection('FCMTokens').doc(tokens[index]).delete();
tokensDelete.push(deleteTask);
}
}
});
return Promise.all(tokensDelete);
}
You need to return a Promise in your Cloud Function, in such a way the Cloud Functions instance running your function does not shut down before your function successfully reaches its terminating condition or state. See the doc for more details.
In your case you are not returning anything in the Cloud Function itself. Since async functions always return a Promise, you can adapt your code as follows:
exports.cambiaColeccion = functions.firestore
.document('sendMessage/{docId}')
.onCreate((snap, context) => {
const nuevoMensaje = snap.data();
console.log('id', nuevoMensaje);
console.log('titulo', nuevoMensaje.titulo)
return enviaMensage();
});
async function enviaMensage() {
console.log('en enviaMensaje');
const payload = {
notification: {
title: "Titulo del mensaje",
body: "Texto del mensaje ... ",
sound: 'default',
badge: '1',
}
}
// Get the list of device tokens.
const allTokens = await admin.firestore().collection('FCMTokens').get();
if (allTokens.size > 0) { // allTokens is a QuerySnapshot
const tokens = allTokens.docs.map(tokenDoc => tokenDoc.id);
await admin.messaging().sendToDevice(tokens, payload);
}
}
Note that it would be good to add some try/catch block in order to capture and debug potential errors.
Update following your comment on the cleanupTokens function.
Your cleanupTokens function is correct. It is asynchronous since it returns a Promise (returned by Promise.all(tokensDelete);).
The way you call it should work correctly:
const allTokens = await admin.firestore().collection('FCMTokens').get();
if (allTokens.size > 0) { // allTokens is a QuerySnapshot
const tokens = allTokens.docs.map(tokenDoc => tokenDoc.id);
const response = await admin.messaging().sendToDevice(tokens, payload);
await cleanupTokens(response, tokens);
}

Expo Push Notifications Generating Duplicate Tokens

SDK Version: 37
Platforms(Android/iOS/web/all): all
Every time I run await Notifications.getExpoPushTokenAsync(); the same token is generated. The token looks like: ExponentPushToken[NgTr99YO5fy0EQM4R]. I am able to get push notifications to this token so I know it is formatted correctly, I am just confused as to why it keeps pushing the same token to me when it should generate a new one. What am I missing?
const registerForPushNotificationsAsync = async () => {
if (Constants.isDevice) {
const {status: existingStatus} = await Permissions.getAsync(
Permissions.NOTIFICATIONS
);
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const {status} = await Permissions.askAsync(Permissions.NOTIFICATIONS);
finalStatus = status;
}
if (finalStatus !== "granted") {
return;
}
token = await Notifications.getExpoPushTokenAsync();
} else {
alert("Must use physical device for Push Notifications");
}
if (Platform.OS === "android") {
Notifications.createChannelAndroidAsync("default", {
name: "default",
sound: true,
priority: "max",
vibrate: [0, 250, 250, 250],
});
}
return token;
};
followed this documentation:
https://docs.expo.io/guides/push-notifications/
My mistake. The duplicate tokens were being created based off of what device i was using to create a new token. In my case I was making multiple profiles on the same device so I was receiving the same token for each profile which was throwing me off.
All is working as it should be.

How do you get to the values of ALL the tokens on your server?

I'd like to get the collection of ALL my FCM user iOS device tokens from this path in my Firebase Database:
BootCamp/Notifications/iOS
At this location, an autoIDChild is created to store the users' device tokens as "deviceToken".
I've been trying to follow the cloud_functions example at this link, but as my use-case is different it's been a little tough to figure out. Here's my cloud-function code in JS:
exports.iOSPush = functions.database.ref('/BootCamp/Bulletins/date').onWrite((snapShot, context) =>{
let tokensSnapShot
let tokens
//here, I attempt to get access to all iOS tokens on my server
const getTokens = admin.database().ref('/BootCamp/Notifications/iOS/{key}').once('value');
return Promise.all([getTokens]).then( (results) => {
tokensSnapShot = results[0]
tokens = Object.keys(tokensSnapShot)
const payload = {
notification:{
title: 'congrats it works',
body: 'Cloud function noti for ios',
sound: 'default',
badge: '1'
}
};
//tokens value in the console log is: "node_,ref_,index_". I was expecting an array of tokens:/
return admin.messaging().sendToDevice(tokens, payload)
})
});
How do I get to these iOS tokens on my server?
It finally occurred to me that I had to name the childPath the same as the device token instead of a randomly generated childID.
Please check this example:
return Promise.all([admin.database().ref(`/users/${user}/account/tokensArray`).once('value')]).then(results => {
const tokens = results[0];
if (!tokens.hasChildren()) return null;
let payload = {
notification: {
title: 'title',
body: 'message',
icon: 'icon-192x192.png'
}
};
const tokensList = Object.keys(tokens.val());
return admin.messaging().sendToDevice(tokensList, payload);
});

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 increase badge count with iOS

I'm developing an app with Firebase and I implemented push notifications right. And now I'm trying to show badge icon but I can't find a good information to do that. I read I should work on the server side and the example code is something like this.
"aps" :
{
"alert" : "Your notification message",
"badge" : badgecount ,
"sound" : "bingbong.aiff"
}
But I don't know where to add the code in my function.
This is how my function looks like:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.pushNotifications = functions.database.ref('/messages/{messageId}')
.onCreate(event => {
const data = event.data;
const fromId = data.fromId;
const toId = data.toId;
const message = data.message;
console.log(fromId + ' sent a message to' + toId);
return admin.database().ref('/users/' + fromId).once('value', snapshot => {
var user = snapshot.val();
var payload = {
notification: {
title: user.username,
body: message
}
}
admin.messaging().sendToDevice(user.fcmToken, payload)
.then(function(response) {
// See the MessagingDevicesResponse reference documentation for
// the contents of response.
console.log("Successfully sent message:", response);
})
.catch(function(error) {
console.log("Error sending message:", error);
});
})
Can someone give me an advice how to do this?
The "apns" object goes in the payload object as a sibling to notification:
var payload = {
notification: {
title: user.username,
body: message
},
apns: {
// badge and other ios only data here here
}
}
I would also suggest returning the admin.messaging() method: return admin.messagins().sendToDevice().ect. It might not be required, but Firebase suggests returning all thenable references.

Resources