Fork me on GitHub

WhatsApp Clone with Meteor and Ionic 2 CLI

Ionic 3 Version (Last Update: 2017-06-15)

FCM Push Notifications

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

In this step we are going to implement push notifications using Google's Firebase Cloud Messaging (FCM). Whenever a user will send you a message, if you don't have our application in the foreground you will get a push notification.

First we will have to create google-services.json in our project's root directory:

16.1 Add google-services.json FCM config google-services.json
1
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
{
  "project_info": {
    "project_number": "152311690748",
    "firebase_url": "https://meteor-c069e.firebaseio.com",
    "project_id": "meteor-c069e",
    "storage_bucket": "meteor-c069e.appspot.com"
  },
  "client": [
    {
      "client_info": {
        "mobilesdk_app_id": "1:152311690748:android:25f0ec3806cf1f01",
        "android_client_info": {
          "package_name": "io.ionic.starter"
        }
      },
      "oauth_client": [
        {
          "client_id": "152311690748-2ht8fdqhlnv8lsrrvnd7u521j9rcgi3h.apps.googleusercontent.com",
          "client_type": 3
        }
      ],
      "api_key": [
        {
          "current_key": "AIzaSyD9CKsY6bC_a4Equ2HpbcrSErgJ2pheDS4"
        }
      ],
      "services": {
        "analytics_service": {
          "status": 1
        },
        "appinvite_service": {
          "status": 1,
          "other_platform_oauth_client": []
        },
        "ads_service": {
          "status": 2
        }
      }
    }
  ],
  "configuration_version": "1"
}

Then we need to install the FCM Cordova plug-in:

$ ionic cordova plugin add cordova-plugin-fcm --save
$ npm install --save @ionic-native/fcm

Then let's add it to app.module.ts:

16.3 Add FCM to app.module.ts src/app/app.module.ts
10
11
12
13
14
15
16
 
78
79
80
81
82
83
84
85
import { Camera } from '@ionic-native/camera';
import { Crop } from '@ionic-native/crop';
import { Contacts } from "@ionic-native/contacts";
import { FCM } from "@ionic-native/fcm";
import { AgmCoreModule } from '@agm/core';
import { MomentModule } from 'angular2-moment';
import { ChatsPage } from '../pages/chats/chats';
...some lines skipped...
    SmsReceiver,
    Camera,
    Crop,
    Contacts,
    FCM
  ]
})
export class AppModule {}

Now we can start adding some FCM logic into ChatsPage:

16.4 Add FCM logic to ChatsPage src/pages/chats/chats.ts
7
8
9
10
11
12
13
 
21
22
23
24
25
26
27
28
 
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
import { MessagesPage } from '../messages/messages';
import { ChatsOptionsComponent } from './chats-options';
import { NewChatComponent } from './new-chat';
import { FCM } from "@ionic-native/fcm";
 
@Component({
  templateUrl: 'chats.html'
...some lines skipped...
    private popoverCtrl: PopoverController,
    private modalCtrl: ModalController,
    private alertCtrl: AlertController,
    private platform: Platform,
    private fcm: FCM) {
    this.senderId = Meteor.userId();
  }
 
...some lines skipped...
        this.chats = this.findChats();
      });
    });
 
    // Notifications
    if (this.platform.is('cordova')) {
      //this.fcm.subscribeToTopic('news');
 
      this.fcm.getToken().then(token => {
        console.log("Registering FCM token on backend");
        MeteorObservable.call('saveFcmToken', token).subscribe({
          next: () => console.log("FCM Token saved"),
          error: err => console.error('Impossible to save FCM token: ', err)
        });
      });
 
      this.fcm.onNotification().subscribe(data => {
        if (data.wasTapped) {
          console.log("Received FCM notification in background");
        } else {
          console.log("Received FCM notification in foreground");
        }
      });
 
      this.fcm.onTokenRefresh().subscribe(token => {
        console.log("Updating FCM token on backend");
        MeteorObservable.call('saveFcmToken', token).subscribe({
          next: () => console.log("FCM Token updated"),
          error: err => console.error('Impossible to update FCM token: ' + err)
        });
      });
    }
  }
 
  findChats(): Observable<Chat[]> {

We used the saveFcmToken Meteor method, so we need to create it first:

16.5 Create the saveFcmToken Meteor method api/server/methods.ts
2
3
4
5
6
7
8
 
95
96
97
98
99
100
101
102
103
104
105
106
import { Messages } from './collections/messages';
import { MessageType, Profile } from './models';
import { check, Match } from 'meteor/check';
import { Users } from "./collections/users";
 
const nonEmptyString = Match.Where((str) => {
  check(str, String);
...some lines skipped...
  },
  countMessages(): number {
    return Messages.collection.find().count();
  },
  saveFcmToken(token: string): void {
    if (!this.userId) throw new Meteor.Error('unauthorized', 'User must be logged-in to call this method');
 
    check(token, nonEmptyString);
 
    Users.collection.update({_id: this.userId}, {$set: {"fcmToken": token}});
  }
});

Since we will soon need the node-fetch package, we will need to install it first:

$ npm install --save node-fetch
$ npm install --save-dev @types/node-fetch

Let's implement our server side service which will actually send the notification:

16.7 Store credentials in settings.json api/private/settings.json
4
5
6
7
8
9
10
11
12
13
    "verificationRetriesWaitTime": 0,
    "adminPhoneNumbers": ["+9721234567", "+97212345678", "+97212345679"],
    "phoneVerificationMasterCode": "1234"
  },
  "private": {
    "fcm": {
      "key": "AIzaSyBnmvN5WNv3rAaLra1RUr9vA5k0pNp0KuY"
    }
  }
}

Now we should edit the AddMessage Meteor method to use our just-created service to send the notification:

16.8 Create server side fcm service api/server/services/fcm.ts
1
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
import fetch from 'node-fetch';
 
export interface FcmNotification {
  title: string;
  text: string;
}
 
export class FcmService {
  private key: string = Meteor.settings.private.fcm.key;
 
  sendNotification(notification: FcmNotification, destination: string) {
    const body = {
      notification: notification,
      to: destination
    };
 
    const options = {
      method: 'POST',
      body: JSON.stringify(body),
      headers: {
        "Content-Type": "application/json",
        Authorization: `key=${this.key}`
      },
    };
 
    return fetch("https://fcm.googleapis.com/fcm/send", options);
  }
}
 
export const fcmService = new FcmService();

Before the Typescript compiler complains, let's update our models:

16.9 Update addMessage Meteor method to send fcm notifications api/server/methods.ts
3
4
5
6
7
8
9
 
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { MessageType, Profile } from './models';
import { check, Match } from 'meteor/check';
import { Users } from "./collections/users";
import { fcmService } from "./services/fcm";
 
const nonEmptyString = Match.Where((str) => {
  check(str, String);
...some lines skipped...
        'Chat doesn\'t exist');
    }
 
    const userId = this.userId;
    const senderName = Users.collection.findOne({_id: userId}).profile.name;
    const memberIds = Chats.collection.findOne({_id: chatId}).memberIds;
    const tokens: string[] = Users.collection.find(
      {
        _id: {$in: memberIds, $nin: [userId]},
        fcmToken: {$exists: true}
      }
    ).map((el) => el.fcmToken);
 
    for (let token of tokens) {
      console.log("Sending FCM notification");
      fcmService.sendNotification({"title": `New message from ${senderName}`, "text": content}, token);
    }
 
    return {
      messageId: Messages.collection.insert({
        chatId: chatId,