Firebase Cloud Messaging notification via service worker
I was pretty amazed by one of the local news media HKET (opens new window), as they was able to push a notification to my mobile phone with certain news update occasionally.
I can only recall, that I've only visited their website before few times, but didn't install any of their mobile apps. After look deep into it, I realized the magic happens in service worker! which I spent time looking at it before, in this article.
Actually, it is quite old technology, a lot big company is using that in their production line already. To help me understand how it works better, I did some research and dive a bit deep into it with a web demo.
# Start with simple web app for adding dogs
I like to use vue and vuetify to start my simple app, please follow the first few part (opens new window) to get this simple setup
# Firebase Cloud Messaging
We've learn some firebase cloud thing before. Firebase Cloud Messaging also have native support for notifications, this will simple use FCM (opens new window) to demo.
setup firebase as:
src/plugins/firebase.ts
:
import firebase from 'firebase/app';
import 'firebase/firestore';
import 'firebase/messaging';
import 'firebase/storage';
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: '<something>',
authDomain: '<something>',
databaseURL: '<something>',
projectId: '<something>',
storageBucket: '<something>',
messagingSenderId: '<something>',
appId: '<something>',
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
// Initialize Cloud Firestore through Firebase
let db = firebase.firestore();
db.enablePersistence({ synchronizeTabs: true });
const storage = firebase.storage();
const messaging = firebase.messaging();
export default {
db,
storage,
messaging,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
src/main.ts
:
...
import './plugins/firebase'; // just need import it from main.ts to take effect
...
2
3
The demo app is consist with 3 pages:
- main page, which shows all dog photos
- post page, which add dog photos
- details page, which shows details of each dog
# main page
src/App.vue
<template>
<v-app>
<v-toolbar>
<v-btn icon v-if="$route.name !== 'home'" @click="$router.go(-1) ">
<v-icon>mdi-arrow-left-thick</v-icon>
</v-btn>
<v-toolbar-title>
<span>Dog Lover</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon v-if="$route.name=='home'" @click="getMessagingToken">
<v-icon>mdi-eye-check</v-icon>
</v-btn>
</v-toolbar>
<v-content>
<router-view></router-view>
</v-content>
</v-app>
</template>
<script lang='js'>
import firebase from '@/plugins/firebase';
import axios from 'axios';
const { messaging } = firebase;
export default {
name: 'App',
components: {},
data() {
return {};
},
mounted() {
this.listenTokenRefresh();
},
methods: {
getMessagingToken() {
messaging
.getToken()
.then(async token => {
if (token) {
const currentMessageToken = window.localStorage.getItem(
'messagingToken'
);
console.log('token will be updated', currentMessageToken != token);
if (currentMessageToken != token) {
await this.saveToken(token);
}
} else {
console.log(
'No Instance ID token available. Request permission to generate one.'
);
this.notificationsPermisionRequest();
}
})
.catch(function(err) {
console.log('An error occurred while retrieving token. ', err);
});
},
notificationsPermisionRequest() {
messaging
.requestPermission()
.then(() => {
this.getMessagingToken();
})
.catch(err => {
console.log('Unable to get permission to notify.', err);
});
},
listenTokenRefresh() {
const currentMessageToken = window.localStorage.getItem('messagingToken');
console.log('currentMessageToken', currentMessageToken);
if (!!currentMessageToken) {
messaging.onTokenRefresh(function() {
messaging
.getToken()
.then(function(token) {
this.saveToken(token);
})
.catch(function(err) {
console.log('Unable to retrieve refreshed token ', err);
});
});
}
},
saveToken(token) {
console.log('tokens', token);
// TODO: having a proper post url
axios
.post(
`https://<your-firebase-host>/GeneralSubscription`,
{ token }
)
.then(response => {
window.localStorage.setItem('messagingToken', token);
console.log(response);
})
.catch(err => {
console.log(err);
});
},
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
Please be noted,
https://<your-firebase-host>/GeneralSubscription
is no yet setup, we will do that in next session
src/views/Home.vue
<template>
<v-container grid-list-xs>
<v-layout row wrap>
<v-flex v-for="(dog, index) in dogs" :key="dog.id" xs12 md6 xl3 pa-2>
<v-card @click="$router.push({name: 'details', params:{ id:dog.id, dogProp:dogs[index] }})">
<v-img height="170" :src="dog.url" aspect-ratio="2.75"></v-img>
<v-card-title primary-title style="padding-top:13px">
<div>
<h3 class="headline">{{ dog.comment }}</h3>
<div>{{ dog.info }}</div>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
<v-btn @click="$router.push({ name: 'post'})" color="red" dark fixed bottom right fab>
<v-icon>mdi-plus-thick</v-icon>
</v-btn>
</v-container>
</template>
<script>
import firebase from '@/plugins/firebase'
export default {
data() {
return {
dogs: []
}
},
mounted() {
// NOTE: this looks insecure to update data to firebase directly, enough for boilerplate tho
firebase.db.collection('dogs').orderBy('created_at', 'desc').onSnapshot((snapShot) => {
this.dogs = [];
snapShot.forEach((dog) => {
this.dogs.push({
id: dog.id,
url: dog.data().url,
comment: dog.data().comment,
info: dog.data().info
})
});
});
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# post page
src/views/mixins/postDog.js
import firebase from '@/plugins/firebase';
import router from '@/router';
export default (url, comment, author) => {
let d = new Date();
let days = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
console.log(firebase.db);
firebase.db
.collection('dogs')
.add({
url,
comment,
info: `Posted by ${author != '' ? author : 'Unknow'} on ${
days[d.getDay()]
}`,
created_at: new Date().getTime(),
})
.then(router.push({ name: 'home' }));
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
src/views/Post.vue
<template>
<v-container grid-list-xs>
<v-layout>
<v-flex>
<div id="spinner_container">
<v-progress-circular
v-if="loading"
v-bind:size="40"
indeterminate
color="pink"
class="spinner"
></v-progress-circular>
</div>
<img :src="this.dogUrl" />
<v-container fluid style="min-height: 0" grid-list-lg>
<v-layout row wrap>
<v-flex xs12>
<v-text-field v-model="title" name="title" label="Describe me" id="title" />
<v-text-field
v-model="author"
name="author"
label="Author"
hint="your name"
id="author"
/>
<v-btn block color="primary" @click="post()">POST A DOG</v-btn>
</v-flex>
</v-layout>
</v-container>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import axios from 'axios'
import postDog from './mixins/postDog.js'
export default {
props: {
pictureUrl: {
default: '',
type: String
}
},
data() {
return {
dogUrl: null,
title: '',
author: '',
loading: true,
}
},
mounted() {
if (this.pictureUrl !== '') {
this.dogUrl = this.pictureUrl;
this.loading = false;
} else {
axios.get('https://dog.ceo/api/breed/appenzeller/images/random').then(response => {
if (response.data.status) {
this.dogUrl = response.data.message;
this.loading = false;
} else {
console.log("Error getting image")
}
})
}
},
methods: {
post() {
postDog(this.dogUrl, this.title, this.author)
}
}
}
</script>
<style scoped>
img {
max-width: 100%;
height: auto;
width: auto\9;
/* ie8 */
}
#spinner_container {
text-align: center;
}
.spinner {
margin: auto;
margin: 4rem;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# details page
src/views/Details.vue
<template>
<v-container grid-list-xs>
<v-layout column v-if="!!dog">
<v-flex>
<v-img :src="dog.url"></v-img>
</v-flex>
<v-flex>
<h1>{{ dog.comment }}</h1>
<p class="subtitle">{{ dog.info }}</p>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import firebase from '@/plugins/firebase'
export default {
props: {
dogProp: {
type: Object,
}
},
data() {
return {
dog: undefined
}
},
mounted() {
if (!!this.dogProp) {
this.dog = this.dogProp
} else {
const id = this.$route.params.id
firebase.db.doc(`dogs/${id}`).get()
.then((doc) => {
if (doc.exists) {
this.dog = doc.data();
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch((error) => {
console.log("Error getting document:", error);
});
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# router
src/router/index.ts
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'home',
component: () =>
import(
/*
webpackChunkName: "home" */ '../views/Home.vue'
),
},
{
path: '/details/:id',
name: 'details',
props: true,
component: () =>
import(
/*
webpackChunkName: "details" */ '../views/Details.vue'
),
},
{
path: '/post',
name: 'post',
props: true,
component: () =>
import(
/* webpackChunkName: "post"
*/ '../views/Post.vue'
),
},
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
export default router;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Setup simple API with serverless
Please get your server key from here as indicated:
we need two triggers:
GeneralSubscription
- An endpoint
- create relationship maps for app instances
- basically subscribe your instance app to FCM with a Instance ID
- details at here (opens new window)
createDog
- On firestore documentation create for dog collection
- send notification for the dog data insert
- check for fcm notification (opens new window)
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import axios from 'axios';
const serverKey =
'<some_key>';
const cors = require('cors')({ origin: true });
admin.initializeApp(functions.config().firebase);
const firestore = admin.firestore();
exports.GeneralSubscription = functions.https.onRequest((request, response) => {
console.log('token', request.body.token);
cors(request, response, function() {
axios
.post(
`https://iid.googleapis.com/iid/v1/${request.body.token}/rel/topics/general`,
{},
{
headers: {
'Content-Type': 'application/json',
Authorization: `key=${serverKey}`,
},
}
)
.then(() => {
return firestore
.collection('tokens')
.add({
token: request.body.token,
})
.then((ref) => {
console.log('Document added ID: ', ref.id);
response.status(200).send(`notifications subscription successful`);
});
})
.catch((err) => {
response.status(500).send({
message: 'Whops! there was an error',
error: err.response,
});
});
});
});
interface Dog {
info: string;
comment: string;
}
exports.createDog = functions.firestore
.document('dogs/{dogId}')
.onCreate((event) => {
const dog = event.data() as Dog;
console.log(dog.comment);
return admin.messaging().sendToTopic(
'general',
{
notification: {
title: dog.info,
body: dog.comment,
click_action: 'https://marvin-push-msg.firebaseapp.com',
icon:
'https://marvin-push-msg.firebaseapp.com/img/icons/apple-touch-icon.png', // could be other icon
},
},
{
priority: 'high',
}
);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# Web Push with service worker
You can find the web push concept here (opens new window)
we did looked at how service worker works before, this will be a bit advance and jumpy
creating a sw specific for firebase, src/firebase-messaging-sw.js
:
importScripts('https://www.gstatic.com/firebasejs/5.6.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.6.0/firebase-messaging.js');
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
workbox.routing.registerRoute(
// please specify your project name
new RegExp(
'https://firebasestorage.googleapis.com/v0/b/<your-project>.firebaseapp.com/.*'
),
new workbox.strategies.StaleWhileRevalidate()
);
firebase.initializeApp({
// you dont need too much details, as this is just a subscription to a sender
messagingSenderId: '<something>',
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function (payload) {
// content below is only for default case, if notification content is specified by FCM, it will be override by firebase sdk
console.log(
'[firebase-messaging-sw.js] Received background message ',
payload
);
// Customize notification here
const notificationTitle = 'Background Message Title';
const notificationOptions = {
body: 'Background Message body.',
icon: '/firebase-logo.png',
};
return self.registration.showNotification(
notificationTitle,
notificationOptions
);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
again, the service worker setup can be referred to this official article (opens new window) again
Note: If you set notification fields in your message payload, your setBackgroundMessageHandler callback is not called, and instead the SDK displays a notification based on your payload.
specify in src/registerServiceWorker.ts
for your vue project
...
register(`${process.env.BASE_URL}firebase-messaging-sw.js`, {
...
2
3
4
also, make sure your vue.config.js
as:
module.exports = {
pwa: {
// configure the workbox plugin
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: 'src/firebase-messaging-sw.js',
},
},
transpileDependencies: ['vuetify'],
};
2
3
4
5
6
7
8
9
10
# Conclusion
Firebase simplified the notification process, it works with different divice including android app / ios app / web app. This app is focusing on web app only which firebase provides solution coming out of box.
alternatively, if you looking for open-source solution, maybe think of: