In this step we are going to implement native address book integration, to automatically show only the users whose numbers are present in our address book.
Ionic 2
is provided by default with a Cordova
plug-in called cordova-plugin-contacts
, which allows us to retrieve the contacts from the address book.
Let's start by installing the Contacts
Cordova
plug-in:
$ ionic cordova plugin add cordova-plugin-contacts --save
$ npm install --save @ionic-native/contacts
Then let's add it to app.module.ts
:
9
10
11
12
13
14
15
76
77
78
79
80
81
82
83
import { SmsReceiver } from "../ionic/sms-receiver";
import { Camera } from '@ionic-native/camera';
import { Crop } from '@ionic-native/crop';
import { Contacts } from "@ionic-native/contacts";
import { AgmCoreModule } from '@agm/core';
import { MomentModule } from 'angular2-moment';
import { ChatsPage } from '../pages/chats/chats';
...some lines skipped...
Sim,
SmsReceiver,
Camera,
Crop,
Contacts
]
})
export class AppModule {}
Since we're going to use Sets
in our code, we will have to set the Typescript
target to es6
or enable downlevelIteration
:
5
6
7
8
9
10
11
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"downlevelIteration": true,
"lib": [
"dom",
"es2015",
Now we can create the appropriate handler in the PhoneService
, we will use it inside the NewChatPage
:
3
4
5
6
7
8
9
13
14
15
16
17
18
19
20
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { Meteor } from 'meteor/meteor';
import { Platform } from 'ionic-angular';
import { Sim } from '@ionic-native/sim';
import { Contact, ContactFieldType, Contacts, IContactField, IContactFindOptions } from "@ionic-native/contacts";
import { SmsReceiver } from "../ionic/sms-receiver";
import * as Bluebird from "bluebird";
import { TWILIO_SMS_NUMBERS } from "api/models";
...some lines skipped...
export class PhoneService {
constructor(private platform: Platform,
private sim: Sim,
private smsReceiver: SmsReceiver,
private contacts: Contacts) {
Bluebird.promisifyAll(this.smsReceiver);
}
...some lines skipped...
}
}
getContactsFromAddressbook(): Promise<string[]> {
const getContacts = (): Promise<Contact[]> => {
if (!this.platform.is('cordova')) {
return Promise.reject(new Error('Cannot get contacts: not cordova.'));
}
const fields: ContactFieldType[] = ["phoneNumbers"];
const options: IContactFindOptions = {
filter: "",
multiple: true,
desiredFields: ["phoneNumbers"],
hasPhoneNumber: true
};
return this.contacts.find(fields, options);
};
const cleanPhoneNumber = (phoneNumber: string): string => {
const phoneNumberNoSpaces: string = phoneNumber.replace(/ /g, '');
if (phoneNumberNoSpaces.charAt(0) === '+') {
return phoneNumberNoSpaces;
} else if (phoneNumberNoSpaces.substring(0, 2) === "00") {
return '+' + phoneNumberNoSpaces.slice(2);
} else {
// Use user's international prefix when absent
// FIXME: update meteor-accounts-phone typings
const prefix: string = (<any>Meteor.user()).phone.number.substring(0, 3);
return prefix + phoneNumberNoSpaces;
}
};
return new Promise((resolve, reject) => {
getContacts()
.then((contacts: Contact[]) => {
const arrayOfArrays: string[][] = contacts
.map((contact: Contact) => {
return contact.phoneNumbers
.filter((phoneNumber: IContactField) => {
return phoneNumber.type === "mobile";
}).map((phoneNumber: IContactField) => {
return cleanPhoneNumber(phoneNumber.value);
}).filter((phoneNumber: string) => {
return phoneNumber.slice(1).match(/^[0-9]+$/) && phoneNumber.length >= 8;
});
});
const flattenedArray: string[] = [].concat(...arrayOfArrays);
const uniqueArray: string[] = [...new Set(flattenedArray)];
resolve(uniqueArray);
})
.catch((e: Error) => {
reject(e);
});
});
}
verify(phoneNumber: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
Accounts.requestPhoneVerification(phoneNumber, (e: Error) => {
5
6
7
8
9
10
11
16
17
18
19
20
21
22
23
24
25
26
27
28
29
31
32
33
34
35
36
37
38
39
40
41
42
43
53
54
55
56
57
58
59
60
61
74
75
76
77
78
79
80
import { MeteorObservable } from 'meteor-rxjs';
import * as _ from 'lodash';
import { Observable, Subscription, BehaviorSubject } from 'rxjs';
import { PhoneService } from "../../services/phone";
@Component({
selector: 'new-chat',
...some lines skipped...
senderId: string;
users: Observable<User[]>;
usersSubscription: Subscription;
contacts: string[] = [];
contactsPromise: Promise<void>;
constructor(
private alertCtrl: AlertController,
private viewCtrl: ViewController,
private platform: Platform,
private phoneService: PhoneService
) {
this.senderId = Meteor.userId();
this.searchPattern = new BehaviorSubject(undefined);
...some lines skipped...
ngOnInit() {
this.observeSearchBar();
this.contactsPromise = this.phoneService.getContactsFromAddressbook()
.then((phoneNumbers: string[]) => {
this.contacts = phoneNumbers;
})
.catch((e: Error) => {
console.error(e.message);
});
}
updateSubscription(newValue) {
...some lines skipped...
this.usersSubscription.unsubscribe();
}
this.contactsPromise.then(() => {
this.usersSubscription = this.subscribeUsers();
});
});
}
...some lines skipped...
subscribeUsers(): Subscription {
// Fetch all users matching search pattern
const subscription = MeteorObservable.subscribe('users', this.searchPattern.getValue(), this.contacts);
const autorun = MeteorObservable.autorun();
return Observable.merge(subscription, autorun).subscribe(() => {
We will have to update the users
publication to filter our results:
5
6
7
8
9
10
11
12
16
17
18
19
20
21
22
23
24
25
26
import { Pictures } from './collections/pictures';
Meteor.publishComposite('users', function(
pattern: string,
contacts: string[]
): PublishCompositeConfig<User> {
if (!this.userId) {
return;
...some lines skipped...
if (pattern) {
selector = {
'profile.name': { $regex: pattern, $options: 'i' },
'phone.number': {$in: contacts}
};
} else {
selector = {'phone.number': {$in: contacts}}
}
return {
Since they are now useless, we can finally remove our fake users from the db initialization:
1
2
3
4
5
6
7
8
9
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
Meteor.startup(() => {
if (Meteor.settings) {
Object.assign(Accounts._options, Meteor.settings['accounts-phone']);
SMS.twilio = Meteor.settings['twilio'];
}
});
Obviously we will have to reset the database to see any effect:
$ npm run api:reset
To test if everything works properly I suggest to create a test user on your PC using a phone number which is already present in your phone's address book.
Let's re-add our fake users and whitelist them in the users
publication for the moment:
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
79
80
81
82
83
84
85
86
import { Meteor } from 'meteor/meteor';
import { Picture } from './models';
import { Accounts } from 'meteor/accounts-base';
import { Users } from './collections/users';
Meteor.startup(() => {
if (Meteor.settings) {
Object.assign(Accounts._options, Meteor.settings['accounts-phone']);
SMS.twilio = Meteor.settings['twilio'];
}
if (Users.collection.find().count() > 0) {
return;
}
let picture = importPictureFromUrl({
name: 'man1.jpg',
url: 'https://randomuser.me/api/portraits/men/1.jpg'
});
Accounts.createUserWithPhone({
phone: '+972540000001',
profile: {
name: 'Ethan Gonzalez',
pictureId: picture._id
}
});
picture = importPictureFromUrl({
name: 'lego1.jpg',
url: 'https://randomuser.me/api/portraits/lego/1.jpg'
});
Accounts.createUserWithPhone({
phone: '+972540000002',
profile: {
name: 'Bryan Wallace',
pictureId: picture._id
}
});
picture = importPictureFromUrl({
name: 'woman1.jpg',
url: 'https://randomuser.me/api/portraits/women/1.jpg'
});
Accounts.createUserWithPhone({
phone: '+972540000003',
profile: {
name: 'Avery Stewart',
pictureId: picture._id
}
});
picture = importPictureFromUrl({
name: 'woman2.jpg',
url: 'https://randomuser.me/api/portraits/women/2.jpg'
});
Accounts.createUserWithPhone({
phone: '+972540000004',
profile: {
name: 'Katie Peterson',
pictureId: picture._id
}
});
picture = importPictureFromUrl({
name: 'man2.jpg',
url: 'https://randomuser.me/api/portraits/men/2.jpg'
});
Accounts.createUserWithPhone({
phone: '+972540000005',
profile: {
name: 'Ray Edwards',
pictureId: picture._id
}
});
});
function importPictureFromUrl(options: { name: string, url: string }): Picture {
const description = { name: options.name };
return Meteor.call('ufsImportURL', options.url, description, 'pictures');
}
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if (pattern) {
selector = {
'profile.name': { $regex: pattern, $options: 'i' },
$or: [
{'phone.number': {$in: contacts}},
{'profile.name': {$in: ['Ethan Gonzalez', 'Bryan Wallace', 'Avery Stewart', 'Katie Peterson', 'Ray Edwards']}}
]
};
} else {
selector = {
$or: [
{'phone.number': {$in: contacts}},
{'profile.name': {$in: ['Ethan Gonzalez', 'Bryan Wallace', 'Avery Stewart', 'Katie Peterson', 'Ray Edwards']}}
]
}
}
return {