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.
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
:
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
:
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();
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) => {
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:
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:
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:
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()
:
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:
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;
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) => {
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
:
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:
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
:
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
:
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 ourStore
in order to fix it.
We will also have to update selectProfilePicture
in the profile Page
to use getPicture
:
36
37
38
39
40
41
42
}
selectProfilePicture(): void {
this.pictureService.getPicture(false, true).then((blob) => {
this.uploadProfilePicture(blob);
})
.catch((e) => {