1

I'm trying to send a notification with Firebase Cloud Functions when someone likes a photo. I've coped the Firebase example showing how to send one when someone follows you and tried to modify it.

The problem is that I need to do another additional query in the function to get the key of the person who liked a photo before I can get their token from their User node. The problem is that the getDeviceTokensPromise somehow is not fulfilled and tokensSnapshot.hasChildren can't be read because it is undefined. How can I fix it?

    exports.sendPhotoLikeNotification = functions.database.ref(`/photo_like_clusters/{photoID}/{likedUserID}`)
        .onWrite((change, context) => {
          const photoID = context.params.photoID;
          const likedUserID = context.params.likedUserID;
          // If un-follow we exit the function
          if (!change.after.val()) {
            return console.log('User ', likedUserID, 'un-liked photo', photoID);
          }

          var tripName;
          var username = change.after.val()

          // The snapshot to the user's tokens.
          let tokensSnapshot;

          // The array containing all the user's tokens.
          let tokens;

          const photoInfoPromise = admin.database().ref(`/photos/${photoID}`).once('value')
            .then(dataSnapshot => {
              tripName = dataSnapshot.val().tripName;
              key = dataSnapshot.val().ownerKey;
              console.log("the key is", key)
              return getDeviceTokensPromise = admin.database()
              .ref(`/users/${key}/notificationTokens`).once('value');
    ///// I need to make sure that this returns the promise used in Promise.all()/////
            });


          return Promise.all([getDeviceTokensPromise, photoInfoPromise]).then(results => {
            console.log(results)
            tokensSnapshot = results[0];

            // Check if there are any device tokens.

//////This is where I get the error that tokensSnapshot is undefined///////
            if (!tokensSnapshot.hasChildren()) {
              return console.log('There are no notification tokens to send to.');
            }

            // Notification details.

            const payload = {
              notification: {
                title: 'Someone liked a photo!',
                body: `${username} liked one of your photos in ${tripName}.`,
              }
            };

        // Listing all tokens as an array.
        tokens = Object.keys(tokensSnapshot.val());
        // Send notifications to all tokens.
        return admin.messaging().sendToDevice(tokens, payload);
      });
Peza
  • 1,347
  • 1
  • 11
  • 23
Cody Lucas
  • 682
  • 1
  • 6
  • 23
  • Here's a working example using the new 1.x codebase. https://hightechtele.com/articles/firestore-trigger-email – Ronnie Royston Jun 09 '18 at 19:06
  • Thank you, but what I really need to know if how to make sure that 'key' is available before starting the deviceTokenPromise – Cody Lucas Jun 09 '18 at 20:31
  • Not sure I understand... The Firebase Cloud Function is basically a node server running in your Firebase Project. Your node script/app (`functions/index.js`) is where you define a trigger. That trigger will pass arguments, e.g. a property or key or whatever that your node script ("Cloud Function") consumes and acts on. – Ronnie Royston Jun 09 '18 at 20:43
  • Yes you're right. When a write is detected on the photo_like_cluster node, the function is triggered. The write has the key of the person that liked the photo and their username. I then need to get the device token of the user to send a notification to. Their UID is available at photos/${photoID}/ownerKey. The photo owner's key just needs to be available before getting the token or it will fail because the UID is undefined. How can I make sure the key is returned before starting the getDeviceTokensPromise? – Cody Lucas Jun 09 '18 at 20:46
  • key of the person that liked the photo is the key to pass to your promise, right? – Ronnie Royston Jun 09 '18 at 20:59
  • The UID of the person that liked the photo is likedUserID. The UID to get the device token for the owner of the photo (the guy getting the notification) is 'key', which is what I need to query and have access to before querying their profile for the device token. So yes - if that's what you mean. – Cody Lucas Jun 09 '18 at 21:03
  • design db so that `likes` is a subcollection of the photo-uid. for every user that clicks like, add their `user-uid:true` to `photo-uid/likes` collection. – Ronnie Royston Jun 09 '18 at 21:18
  • 1
    Why are you using `Promise.all` at all? Your `/photos/${photoID}` and `/users/${key}/notificationTokens` queries will need to happen sequentially anyway. I suspect your root problem is [How do I access previous promise results in a `.then()` chain?](https://stackoverflow.com/q/28250680/1048572). Do **not** use outer-scope variables like `tripName`, `tokensSnapshot` and `tokens` that get initialised at any later time. Try to write it with `const` only (initialise variables where you declare them) and nesting when necessary. – Bergi Jun 09 '18 at 21:29
  • @Bergi I could kiss you, that's exactly what I needed to learn. The notification goes through now. I didn't know I could break it up like that and call .then later. If you'd like to answer the question I'll mark it as correct. – Cody Lucas Jun 09 '18 at 22:04

1 Answers1

0

Ok so based on your comment, here's the updated answer.

You need to chain your promises and you don't need to use Promise.all() - that function is used for a collection of asynchronous tasks that all need to complete (in parallel) before you take your next action.

Your solution should look like this:

admin.database().ref(`/photos/${photoID}`).once('value')
    .then(dataSnapshot => {
        // Extract your data here
        const key = dataSnapshot.val().ownerKey;

        // Do your other stuff here
        // Now perform your next query
        return admin.database().ref(`/users/${key}/notificationTokens`).once('value');
    })

    .then(tokenSnapshot => {
        // Here you can use token Snapshot
    })

    .catch(error => {
        console.log(error);
    });

Because your second network request depends on the completion of the first, chain your promises by appending a second .then() after your first and don't terminate it with a semi-colon ;. If the new promise created within first .then() scope resolves it will call the second .then() function.

If you need to access variables from the first .then() scope in the second, then you must declare them in the general function, and assign them in the closure scopes.

You can check out more info on promise chaining here.

Peza
  • 1,347
  • 1
  • 11
  • 23
  • Thanks for the reply. Yeah, that's what I'm struggling with. In the post I had mentioned: "The problem is that I need to do another additional query in the function to get the key of the person who liked a photo before I can get their token from their User node". How would I go about getting the key from the query in the post, then using that key for the token promise? – Cody Lucas Jun 09 '18 at 20:16
  • Is there way to just define the getDeviceTokensPromise before I know 'key', then resolving that promise after key is defined and able to be used? – Cody Lucas Jun 09 '18 at 21:20
  • Oh right, sorry I misread that based on you using `Promise.all()`. I will update my answer now. What you want to do is chain promises so that you can take action on the completion of a query returning. See updated answer in about 5 mins. – Peza Jun 09 '18 at 22:54