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

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.

Related

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.

Flutter calling firebase cloud function admin.auth.updateUser

EDIT**
Ok so I was able to get the parameters working thanks to first answer provided but now I have an issue whereby my function is creating a new user entirely in Firebase and not update an existing one, the uid that i am passing into the auth.admin.updateUser is teh uid of the existing user who's email i want to update. Here is the updated cloud function which is adding a new user rather than updating the existing:
exports.updateEmail = functions.https.onCall((data, context) => {
const email = data.email;
const uid = data.uid;
admin.auth().updateUser(uid, {
email: email
})
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully updated user", userRecord.toJSON());
return response.status(200).json(userRecord.toJSON());
})
.catch(function(error) {
console.log("Error updating user:", error);
return response.status(404).json({
error: 'Something went wrong.'
});
});
});
I got the function from the firebase docs but it isn't doing what I intended it to do.
ORIGINAL POST**
I'm having some difficulty getting a cloud function to work when calling the function from within my flutter code. The issue that I am having is that the uid and email fields are undefined even though I am passing them through to the cloud function using busboy fields.
I'm trying to pass the email and uid field though to the function as follows:
final request = http.MultipartRequest('POST', Uri.parse('****************my function url************'));
request.fields['email'] = Uri.encodeComponent(newEmail);
request.fields['uid'] = Uri.encodeComponent(selectedUser.uid);
request.headers['Authorization'] = 'Bearer ${_authenticatedUser.token}';
final http.StreamedResponse streamedResponse = await request.send();
And on the Node.js side I am trying to use these fields using busboy, here is my cloud function in Node.js:
exports.changeEmail = functions.https.onRequest((request, response) => {
if (!request.headers.authorization ||
!request.headers.authorization.startsWith('Bearer ')
) {
return response.status(401).json({
error: 'Unauthorized.'
});
}
let idToken;
idToken = request.headers.authorization.split('Bearer ')[1];
let email;
let uid;
const busboy = new Busboy({
headers: request.headers
});
busboy.on('field', (fieldname, value) => {
if (fieldname == 'email') {
email = decodeURIComponent(value);
}
if (fieldname == 'uid') {
uid = decodeURIComponent(value);
}
});
admin.auth().updateUser(uid, {
email: email
})
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully updated user", userRecord.toJSON());
return response.status(200).json(userRecord.toJSON());
})
.catch(function(error) {
console.log("Error updating user:", error);
return response.status(404).json({
error: 'Something went wrong.'
});
});
});
Even though I am passing the fields in with busboy fields they are not getting set in the function, is there something I am doing wrong here?
Why don't you use a callable function? It will automatically receive the authentication data.
The documentation even has examples on how to get the uid and email:
Declare the function:
exports.addMessage = functions.https.onCall((data, context) => {
// ...
});
Get the user properties from the context parameter:
// Message text passed from the client.
const text = data.text;
// Authentication / user information is automatically added to the request.
const uid = context.auth.uid;
const name = context.auth.token.name || null;
const picture = context.auth.token.picture || null;
const email = context.auth.token.email || null;
Call the function from your Flutter code:
Install cloud_functions package and then:
import 'package:cloud_functions/cloud_functions.dart';
await CloudFunctions.instance.call(functionName: 'addMessage');
If the user is authenticated before calling the function that's all you need to do.
You can also pass additional parameters to the function:
await CloudFunctions.instance.call(functionName: 'addMessage', parameters: {"email": "whatever#example.com"});
Any parameters will be passed to the data parameter on the function side.

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);
});

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.

Notifications not showing up on database trigger

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
}
};

Resources