2018年12月11日 星期二

[Vue] Shopcart example with Firebase Cloud Messaging and Functions (3)


 Vue.js    VueFire    FCM    Topics    Cloud Functions 








Introduction


FCM provides 2 ways for sending message to multiple devices:

The difference for the 2 methods is as following (based on the official document on Dec, 2018)


Topic messaging
Device group messaging
Description
Based on the publish/subscribe model, FCM topic messaging allows you to send a message to multiple devices that have been subscribed to a particular topic.
It’s Optimized for throughput rather than latency
All devices in a group share a common notification key, which is the token that FCM uses to fan out messages to all devices in the group.
Scenario
Broadcast discount or gaming scores to users
l   Sending messages to multiple devices per user
l   Sending a message to a specific circle of friends.
Limits
l   One app instance can be subscribed to no more than 2000 topics
l   Unlimited subscriptions for each topic
In other words, no more than 20 devices in a single device group


We will use Topic messaging to broadcast a Confirmed Order from one user to others. (Something like the popup message on Booking.com to notify other’s reservation on the hotel you are watching)



Environment


Vue.js 2.5.11
Vue CLI 3.2.1
Firebase Javascript SDK 5.5.8
VueFire 1.4.5


Google Instance ID API


The first step that we have to know how to subscribe/unsubscribe a topic thru Google Instance ID API.

Subscribe topic

Subscribe one device to a topic

Http method
POST
Url
https://iid.googleapis.com/iid/v1/<registration token>/rel/topics/<topic name>
Header
Authorization
key=<Server key>


Subscribe devices to a topic

Http method
POST
Url
https://iid.googleapis.com/iid/v1:batchAdd
Header
Authorization
key=<Server key>
Content-Type
application/json
Body
{
"to": "/topics/<topic name>",
"registration_tokens": [
       "<registration token 1>",
   "<registration token 2>",
  
]
}


Unsubbscribe topic

Http method
POST
Url
https://iid.googleapis.com/iid/v1:batchRemove
Header
Authorization
key=<Server key>
Content-Type
application/json
Body
{
"to": "/topics/<topic name>",
"registration_tokens": [
       "<registration token 1>",
   "<registration token 2>",
  
]
}



Get information

To see what topics had been subscribed, we can use the Get-information-about-app-instances API.

Http method
GET
Url
https://iid.googleapis.com/iid/info/<registration token>?details=true
Header
Authorization
key=<Server key>


Which will response the JSON indicates what topics the device subscribed.

{
  "connectDate": "2018-12-10",
  "application": "com.chrome.windows",
  "subtype": "wp:http://localhost:8081/#123CD065-4567-4138-9735-B83EB0D55-V2",
  "scope": "*",
  "authorizedEntity": "1234567890",
  "rel": {
      "topics": {
          "orders": {
              "addDate": "2018-12-10"
          }
      }
  },
  "connectionType": "WIFI",
  "platform": "BROWSER"
}




Implement: Cloud Functions with Google Instance ID API


Now we can implement the Firebase Cloud Functions to support subscription/un-subscription with Google Instance ID API.

First, We must install request(Simplified HTTP client),

$ npm install request

and create firebase.config.js to store the Server key.

var firebaseConfig = {
    serverKey: "<Your project's Cloud Messaging Server key>"
};

module.exports = firebaseConfig;


Subscription

const functions = require("firebase-functions")
const cors = require("cors")({ origin: true });
const httpClient = require("request");
const firebaseConfig = require("./firebase.config")

exports.subscribeTopic = functions.https.onRequest((request, response) => {

    cors(request, response, () => {
        const userToken = request.get("token");
        const topic = request.query.topic;

        if (userToken) { //Subscribe ONE token to a topic

            httpClient({
                url: `https://iid.googleapis.com/iid/v1/${userToken}/rel/topics/${topic}`,
                method: 'POST',
                headers: {
                    Authorization: "key=" + firebaseConfig.serverKey
                }
            }, function (err, res, body) {
                if (err) {
                    console.log(err);
                    response.send("Failed to subscribe topic: " + topic);
                } else {
                    console.log(res.statusCode, body);
                    response.send("Successfully subscribed to topic: " + topic);
                }
            });
        }
        else {  //Subscribe MULTIPLE tokens to a topic

            let userTokens = request.body.tokens; //tokens should be Array

            // 1. Google API
            let data = '{ "to": "/topics/' + topic + '", "registration_tokens": ' + JSON.stringify(userTokens) + '}';

            httpClient({
                url: "https://iid.googleapis.com/iid/v1:batchAdd",
                method: 'POST',
                headers: {
                    Authorization: "key=" + firebaseConfig.serverKey
                },
                body: data
            }, function (err, res, body) {
                if (err) {
                    console.log(err);
                    response.send("Failed to subscribe topic: " + topic);
                } else {
                    console.log(res.statusCode, body);
                    response.send("Successfully subscribed to topic: " + topic);
                }
            });
        }
    });
})


Demo: single-device subscription

Http method
POST
Url
https://xxxx.cloudfunctions.net/subscribeTopic?topic=<topic name>
Header
token
<registration token>

Demo: multiple-devices subscription

Http method
POST
Url
https://xxxx.cloudfunctions.net/subscribeTopic?topic=orders

Content-Type
application/json
Body
{
"tokens": [
       "<registration token 1>",
   "<registration token 2>",
  
]
}


Unsubscription

const functions = require("firebase-functions")
const cors = require("cors")({ origin: true });
const httpClient = require("request");
const firebaseConfig = require("./firebase.config")

exports.unsubscribeTopic = functions.https.onRequest((request, response) => {

    const userTokens = request.body.tokens; //tokens should be Array
    const topic = request.query.topic;

    cors(request, response, () => {
       
        //1. By Google API
        let data = '{ "to": "/topics/' + topic + '", "registration_tokens": ' + JSON.stringify(userTokens) + '}';

        httpClient({
            url: "https://iid.googleapis.com/iid/v1:batchRemove",
            method: 'POST',
            headers: {
                Authorization: "key=" + firebaseConfig.serverKey
            },
            body: data
        }, function (err, res, body) {
            if (err) {
                console.log(err);
                response.send("Failed to unsubscribe topic: " + topic);
            } else {
                console.log(res.statusCode, body);
                response.send("Successfully unsubscribed to topic: " + topic);
            }
        });
    });
})


Demo: multiple-devices unsubscription

Http method
POST
Url
https://xxxx.cloudfunctions.net/unsubscribeTopic?topic=orders

Content-Type
application/json
Body
{
"tokens": [
       "<registration token 1>",
   "<registration token 2>",
  
]
}




Implement: Cloud Functions with Firebase Admin SDK


However, we can use the Firebase Admin SDK (interface: admin.messaging.Messaging) to do the subscription/un-subscription as well.

Subscription

const functions = require("firebase-functions")
const admin = require("firebase-admin")
const cors = require("cors")({ origin: true });
const firebaseConfig = require("./firebase.config")

exports.subscribeTopic = functions.https.onRequest((request, response) => {

    cors(request, response, () => {
        const topic = request.query.topic;
const userTokens = request.body.tokens; //tokens should be Array

        response.end();
        return admin.messaging().subscribeToTopic(userTokens, topic)
                .then(function (response) {
                    console.log('Successfully subscribed to topic:', response);
                })
                .catch(function (err) {
                    console.error("Failed to subscribe topic: " + topic, err);
        });
    });
})


Unsubscription

const functions = require("firebase-functions")
const admin = require("firebase-admin")
const cors = require("cors")({ origin: true });
const firebaseConfig = require("./firebase.config")

exports.unsubscribeTopic = functions.https.onRequest((request, response) => {

    const userTokens = request.body.tokens; //tokens should be Array
    const topic = request.query.topic;

    cors(request, response, () => {
        response.end();
       
        return admin.messaging().unsubscribeFromTopic(userTokens, topic)
            .then(function (response) {
                console.log('Successfully subscribed to topic:', response);
            })
            .catch(function (err) {
                console.error("Failed to unsubscribe topic: " + topic, err);
            });
    });
})


So far we had implemented the Cloud Functions to subscribe/unsubscribe topic.
You can try implement the client side like doing subscription on certain topics when a device first get or refresh it’s token.

For example, call the following method when the registration token is known.

subscribeTopic(token, topic) {
      console.log("Start subscribe topic with token: " + token);
      this.axios
        .get(
          "https://us-central1-shopcart-vue.cloudfunctions.net/subscribeTopic?topic=" + topic,
          {
            headers: {
              token: token
            }
          }
        )
        .then(result => {
          console.log(result);
        });
    }



Next step, we will create a cloud function which will send message to the topic: “orders”.


Implement: Send message to topic



We had used the method: admin.messaging().send(…) for send message to specified registration token(s) on previous article, [Vue] Shopcart example with Firebase Cloud Messaging and Functions (2)

We will use the same method but replace the token(s) to the topic.

exports.sendOrdersMsg = functions.https.onRequest((request, response) => {
    cors(request, response, () => {
        const userName = request.get("user-name")
        const itemsCnt = request.query.itemscnt;
        response.end();

        let message = {
            webpush: {
                notification: {
                    title: "Orders",
                    body: `${userName} just bought ${itemsCnt} awesome product(s), don't miss the best discount!`,
                    click_action: "https://shopcart-vue.firebaseapp.com",
                    icon: "dist/firebase-logo.png"
                }
            },
            topic: "orders"
        };

        return admin.messaging().send(message)
            .then((response) => {
                // Response is a message ID string.
                console.log('Successfully sent message:', response);
            })
            .catch((error) => {
                console.log('Error sending message:', error);
            });
    });
})


This cloud function should be requested whenever a user have successfully confirmed (saved) the order in the shopping cart page.
For example, add the following function to trigger the cloud function in Vue.js app.


export default {
  name: "shopcart",
  //…skip
  methods: {
    pushOrdersMsg(user, itemsCnt) {
      console.log(`Start push msg with user: ${user}, items: ${itemsCnt}`);
      this.axios
        .get(
          "https://xxxx.cloudfunctions.net/sendOrdersMsg" + "?itemscnt=" + itemsCnt,
          {
            headers: {
              "user-name": user,
            }
          }
        )
        .then(result => {
          console.log(result);
        });
    }
};


Optional: Use RTDB triggers or Firestore triggers

If we do not want to update the code of client side.
We can use RTDB triggers or Firestore triggers to trigger the cloud function instead.

I will take RTDB triggers for example since my shopcart app uses RTDB :P

Modify the original cloud function: sendOrdersMsg, as following,


exports.sendOrdersMsg = functions.database.ref('/Demo/orders/{pushId}')
    .onCreate((snapshot, context) => {
        // Grab the current value of what was written to the Realtime Database.
        const newOrder = snapshot.val();

        console.info("Push id", context.params.pushId); //You can the param: pushId, by context.params.pushId
        const user = newOrder.customer;
        const itemsCnt = newOrder.items.length;

        let message = {
            webpush: {
                notification: {
                    title: "Orders",
                    body: `${user} just bought ${itemsCnt} awesome product(s), don't miss the best discount!`,
                    click_action: "https://shopcart-vue.firebaseapp.com",
                    icon: "dist/firebase-logo.png"
                }
            },
            topic: "orders"
        };

        return admin.messaging().send(message)
            .then((response) => {
                // Response is a message ID string.
                console.log('Successfully sent message:', response);
            })
            .catch((error) => {
                console.log('Error sending message:', error);
            });
    });

Then this cloud functions will be triggered when a new data is written to /Demo/orders/







Demo



I also received the FCM message from my mobile phone which was logged in as another Google account.




Reference




沒有留言:

張貼留言