Fork me on GitHub

WhatsApp Clone with Meteor and Ionic 2 CLI

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

Native Mobile

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

In this step, we will be implementing additional native features like automatic phone number detection and access to the device's camera, to enhance the user experience.

Automatic phone number detection

Ionic 2 is provided by default with a Cordova plug-in called cordova-plugin-sim, which allows us to retrieve some data from the current device's SIM card, if even exists. We will use the SIM card to automatically detect the current device's phone number, so this way the user won't need to manually fill-in his phone number whenever he tries to login.

Let's start installing the Sim Cordova plug-in:

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

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

14.2 Add Sim to app.module.ts src/app/app.module.ts
5
6
7
8
9
10
11
 
68
69
70
71
72
73
74
75
import { StatusBar } from '@ionic-native/status-bar';
import { Geolocation } from '@ionic-native/geolocation';
import { ImagePicker } from '@ionic-native/image-picker';
import { Sim } from '@ionic-native/sim';
import { AgmCoreModule } from '@agm/core';
import { MomentModule } from 'angular2-moment';
import { ChatsPage } from '../pages/chats/chats';
...some lines skipped...
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    PhoneService,
    ImagePicker,
    PictureService,
    Sim
  ]
})
export class AppModule {}

Let's add the appropriate handler in the PhoneService, we will use it inside the LoginPage:

14.3 Use getNumber native method src/pages/login/login.ts
1
2
3
4
 
7
8
9
10
11
12
13
 
16
17
18
19
20
21
22
23
24
25
26
27
import { Component, AfterContentInit } from '@angular/core';
import { Alert, AlertController, NavController } from 'ionic-angular';
import { PhoneService } from '../../services/phone';
import { VerificationPage } from '../verification/verification';
...some lines skipped...
  selector: 'login',
  templateUrl: 'login.html'
})
export class LoginPage implements AfterContentInit {
  private phone = '';
 
  constructor(
...some lines skipped...
    private navCtrl: NavController
  ) {}
 
  ngAfterContentInit() {
    this.phoneService.getNumber()
      .then((phone) => this.phone = phone)
      .catch((e) => console.error(e.message));
  }
 
  onInputKeypress({keyCode}: KeyboardEvent): void {
    if (keyCode === 13) {
      this.login();
14.4 Implement getNumber with native ionic src/services/phone.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
31
32
import { Injectable } from '@angular/core';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { Platform } from 'ionic-angular';
import { Sim } from '@ionic-native/sim';
 
@Injectable()
export class PhoneService {
  constructor(private platform: Platform,
              private sim: Sim) {
 
  }
 
  async getNumber(): Promise<string> {
    if (!this.platform.is('cordova')) {
      throw new Error('Cannot read SIM, platform is not Cordova.')
    }
 
    if (!(await this.sim.hasReadPermission())) {
      try {
        await this.sim.requestReadPermission();
      } catch (e) {
        throw new Error('User denied SIM access.');
      }
    }
 
    return '+' + (await this.sim.getSimInfo()).phoneNumber;
  }
 
  verify(phoneNumber: string): Promise<void> {
    return new Promise<void>((resolve, reject) => {
      Accounts.requestPhoneVerification(phoneNumber, (e: Error) => {

SMS OTP autofill

On supported platforms (Android) it would be nice to automatically detect the incoming OTP (One Time Password) SMS and fill the verification field in place of the user.

We need to add the Cordova plugin first:

$ ionic cordova plugin add cordova-plugin-sms-receiver --save

Then we must create the corresponding ionic-native plugin, since no one created it:

14.6 Added ionic-native plugin for reading SMS OTP src/ionic/sms-receiver/index.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
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
import {Injectable} from '@angular/core';
import {Cordova, Plugin, IonicNativePlugin} from '@ionic-native/core';
 
 
/**
 * @name SmsReceiver
 * @description
 * Allows you to receive incoming SMS. You have the possibility to start and stop the message broadcasting.
 *
 * Requires Cordova plugin: `cordova-plugin-smsreceiver`. For more info, please see the [Cordova SmsReceiver docs](https://github.com/ahmedwahba/cordova-plugin-smsreceiver).
 *
 * @usage
 * ```typescript
 * import { SmsReceiver } from '@ionic-native/smsreceiver';
 *
 *
 * constructor(private smsReceiver: SmsReceiver) { }
 *
 * ...
 *
 * this.smsReceiver.isSupported().then(
 *   (supported) => console.log('Permission granted'),
 *   (err) => console.log('Permission denied: ', err)
 * );
 *
 * this.smsReceiver.startReceiving().then(
 *   (msg) => console.log('Message received: ', msg)
 * );
 *
 * this.smsReceiver.stopReceiving().then(
 *   () => console.log('Stopped receiving'),
 *   (err) => console.log('Error: ', err)
 * );
 * ```
 */
@Plugin({
  pluginName: 'SmsReceiver',
  plugin: 'cordova-plugin-smsreceiver',
  pluginRef: 'sms',
  repo: 'https://github.com/ahmedwahba/cordova-plugin-smsreceiver',
  platforms: ['Android']
})
@Injectable()
export class SmsReceiver extends IonicNativePlugin {
  /**
   * Check if the SMS permission is granted and SMS technology is supported by the device.
   * In case of Marshmallow devices, it requests permission from user.
   * @returns {void}
   */
  @Cordova()
  isSupported(callback: (supported: boolean) => void, error: () => void): void {
    return;
  }
 
  /**
   * Start the SMS receiver waiting for incoming message.
   * The success callback function will be called every time a new message is received.
   * The error callback is called if an error occurs.
   * @returns {void}
   */
  @Cordova({
    platforms: ['Android']
  })
  startReceiving(callback: (msg: string) => void, error: () => void): void {
    return;
  }
 
  /**
   * Stop the SMS receiver.
   * @returns {void}
   */
  @Cordova({
    platforms: ['Android']
  })
  stopReceiving(callback: () => void, error: () => void): void {
    return;
  }
}

Last but not the least we must import it into app.module.ts as usual:

14.7 Add SmsReceiver to app.module.ts src/app/app.module.ts
6
7
8
9
10
11
12
 
70
71
72
73
74
75
76
77
import { Geolocation } from '@ionic-native/geolocation';
import { ImagePicker } from '@ionic-native/image-picker';
import { Sim } from '@ionic-native/sim';
import { SmsReceiver } from "../ionic/sms-receiver";
import { AgmCoreModule } from '@agm/core';
import { MomentModule } from 'angular2-moment';
import { ChatsPage } from '../pages/chats/chats';
...some lines skipped...
    PhoneService,
    ImagePicker,
    PictureService,
    Sim,
    SmsReceiver
  ]
})
export class AppModule {}

Let's start by using the yet-to-be-created method in the verification page:

14.8 Use getSMS method in verification.ts src/pages/verification/verification.ts
1
2
3
4
 
7
8
9
10
11
12
13
 
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { AfterContentInit, Component, OnInit } from '@angular/core';
import { AlertController, NavController, NavParams } from 'ionic-angular';
import { PhoneService } from '../../services/phone';
import { ProfilePage } from '../profile/profile';
...some lines skipped...
  selector: 'verification',
  templateUrl: 'verification.html'
})
export class VerificationPage implements OnInit, AfterContentInit {
  private code: string = '';
  private phone: string;
 
...some lines skipped...
    this.phone = this.navParams.get('phone');
  }
 
  ngAfterContentInit() {
    this.phoneService.getSMS()
      .then((code: string) => {
        this.code = code;
        this.verify();
      })
      .catch((e: Error) => {
        if (e) {
          console.error(e.message);
        }
      });
  }
 
  onInputKeypress({keyCode}: KeyboardEvent): void {
    if (keyCode === 13) {
      this.verify();

We will need bluebird to promisify sms-receiver:

$ npm install --save bluebird
$ npm install --save-dev @types/bluebird

We will need to add support for es2016 in Typescript, because we will use Array.prototype.includes():

14.10 Added support for es2016 in Typescript tsconfig.json
7
8
9
10
11
12
13
14
    "experimentalDecorators": true,
    "lib": [
      "dom",
      "es2015",
      "es2016"
    ],
    "module": "commonjs",
    "moduleResolution": "node",

Now we can implement the method in the phone service:

14.11 Add getSMS method to phone.ts api/server/models.ts
1
2
3
4
5
export const DEFAULT_PICTURE_URL = '/assets/default-profile-pic.svg';
export const TWILIO_SMS_NUMBERS = ["+12248032362"];
 
export interface Profile {
  name?: string;
14.11 Add getSMS method to phone.ts src/services/phone.ts
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
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
import { Meteor } from 'meteor/meteor';
import { Platform } from 'ionic-angular';
import { Sim } from '@ionic-native/sim';
import { SmsReceiver } from "../ionic/sms-receiver";
import * as Bluebird from "bluebird";
import { TWILIO_SMS_NUMBERS } from "api/models";
import { Observable } from "rxjs";
 
@Injectable()
export class PhoneService {
  constructor(private platform: Platform,
              private sim: Sim,
              private smsReceiver: SmsReceiver) {
    Bluebird.promisifyAll(this.smsReceiver);
  }
 
  async getNumber(): Promise<string> {
...some lines skipped...
    return '+' + (await this.sim.getSimInfo()).phoneNumber;
  }
 
  async getSMS(): Promise<string> {
    if (!this.platform.is('android')) {
      throw new Error('Cannot read SMS, platform is not Android.')
    }
 
    try {
      await (<any>this.smsReceiver).isSupported();
    } catch (e) {
      throw new Error('User denied SMS access.');
    }
 
    const startObs = Observable.fromPromise((<any>this.smsReceiver).startReceiving()).map((msg: string) => msg);
    const timeoutObs = Observable.interval(120000).take(1).map(() => {
      throw new Error('Receiving SMS timed out.')
    });
 
    try {
      var msg = await startObs.takeUntil(timeoutObs).toPromise();
    } catch (e) {
      await (<any>this.smsReceiver).stopReceiving();
      throw e;
    }
 
    await (<any>this.smsReceiver).stopReceiving();
 
    if (TWILIO_SMS_NUMBERS.includes(msg.split(">")[0])) {
      return msg.substr(msg.length - 4);
    } else {
      throw new Error('Sender is not a Twilio number.')
    }
  }
 
  verify(phoneNumber: string): Promise<void> {
    return new Promise<void>((resolve, reject) => {
      Accounts.requestPhoneVerification(phoneNumber, (e: Error) => {

Camera

Next - we will grant access to the device's camera so we can send pictures which are yet to exist in the gallery. Since the Camera plugin can also access the gallery, we will replace the previously used ImagePicker plugin with Camera, which is better maintained and allows for more code reuse. We will also use the Crop plugin to force a 1:1 aspect ratio, when needed.

We will start by adding the appropriate Cordova plug-ins:

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

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

14.13 Add Camera and Crop to app.module.ts src/app/app.module.ts
7
8
9
10
11
12
13
14
 
73
74
75
76
77
78
79
80
81
import { ImagePicker } from '@ionic-native/image-picker';
import { Sim } from '@ionic-native/sim';
import { SmsReceiver } from "../ionic/sms-receiver";
import { Camera } from '@ionic-native/camera';
import { Crop } from '@ionic-native/crop';
import { AgmCoreModule } from '@agm/core';
import { MomentModule } from 'angular2-moment';
import { ChatsPage } from '../pages/chats/chats';
...some lines skipped...
    ImagePicker,
    PictureService,
    Sim,
    SmsReceiver,
    Camera,
    Crop
  ]
})
export class AppModule {}

We will bind the click event in the view:

14.14 Use the new sendPicture method in the template src/pages/messages/messages-attachments.html
1
2
3
4
5
6
7
8
9
10
11
<ion-content class="messages-attachments-page-content">
  <ion-list class="attachments">
    <button ion-item class="attachment attachment-gallery" (click)="sendPicture(false)">
      <ion-icon name="images" class="attachment-icon"></ion-icon>
      <div class="attachment-name">Gallery</div>
    </button>
 
    <button ion-item class="attachment attachment-camera" (click)="sendPicture(true)">
      <ion-icon name="camera" class="attachment-icon"></ion-icon>
      <div class="attachment-name">Camera</div>
    </button>

And we will create the event handler in MessagesAttachmentsComponent:

14.15 Use the getPicture method into messages-attachment.ts src/pages/messages/messages-attachments.ts
1
2
3
4
5
 
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
 
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { Component } from '@angular/core';
import { AlertController, ModalController, Platform, ViewController } from 'ionic-angular';
import { NewLocationMessageComponent } from './location-message';
import { MessageType } from 'api/models';
import { PictureService } from '../../services/picture';
...some lines skipped...
  constructor(
    private viewCtrl: ViewController,
    private modelCtrl: ModalController,
    private pictureService: PictureService,
    private platform: Platform,
    private alertCtrl: AlertController
  ) {}
 
  sendPicture(camera: boolean): void {
    if (camera && !this.platform.is('cordova')) {
      return console.warn('Device must run cordova in order to take pictures');
    }
 
    this.pictureService.getPicture(camera, false)
      .then((blob: File) => {
        this.viewCtrl.dismiss({
          messageType: MessageType.PICTURE,
          selectedPicture: blob
        });
      })
      .catch((e) => {
        this.handleError(e);
      });
  }
 
  sendLocation(): void {
...some lines skipped...
 
    locationModal.present();
  }
 
  handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

Finally we can create a new method in the PictureService to take some pictures and remove the old method which used ImagePicker:

14.16 Implement getPicture method in picture service src/services/picture.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { Injectable } from '@angular/core';
import { Platform } from 'ionic-angular';
import { UploadFS } from 'meteor/jalik:ufs';
import { PicturesStore } from 'api/collections';
import * as _ from 'lodash';
import { DEFAULT_PICTURE_URL } from 'api/models';
import { Camera, CameraOptions } from '@ionic-native/camera';
import { Crop } from '@ionic-native/crop';
 
@Injectable()
export class PictureService {
  constructor(private platform: Platform,
              private camera: Camera,
              private crop: Crop) {
  }
 
  getPicture(camera: boolean, crop: boolean): Promise<File> {
    if (!this.platform.is('cordova')) {
      return new Promise((resolve, reject) => {
        //TODO: add javascript image crop
        if (camera === true) {
          reject(new Error("Can't access the camera on Browser"));
        } else {
          try {
            UploadFS.selectFile((file: File) => {
              resolve(file);
            });
          } catch (e) {
            reject(e);
          }
        }
      });
    }
 
    return this.camera.getPicture(<CameraOptions>{
      destinationType: 1,
      quality: 50,
      correctOrientation: true,
      saveToPhotoAlbum: false,
      sourceType: camera ? 1 : 0
    })
      .then((fileURI) => {
        return crop ? this.crop.crop(fileURI, {quality: 50}) : fileURI;
      })
      .then((croppedFileURI) => {
        return this.convertURLtoBlob(croppedFileURI);
      });
  }
 
  upload(blob: File): Promise<any> {

Choosing to take the picture from the camera instead of the gallery is as simple as passing a boolean parameter to the method. The same is true for cropping.

NOTE: even if the client will not crop the image when passing false, the server will still crop it. Eventually, we will need to edit our Store in order to fix it.

We will also have to update selectProfilePicture in the profile Page to use getPicture:

14.17 Update selectProfilePicture in profile.ts to use getPicture src/pages/profile/profile.ts
36
37
38
39
40
41
42
  }
 
  selectProfilePicture(): void {
    this.pictureService.getPicture(false, true).then((blob) => {
      this.uploadProfilePicture(blob);
    })
      .catch((e) => {