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 {