Authentication

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

Meteor Accounts (Server Side)

In this step we will authenticate and identify users in our app.

Before we go ahead and start extending our app, we will add a few packages which will make our lives a bit less complex when it comes to authentication and users management.

First we will update our Meteor server and add few Meteor packages called accounts-base and accounts-phone which will give us the ability to verify a user using an SMS code, so run the following inside api directory:

$ meteor add accounts-base
$ meteor add npm-bcrypt
$ meteor add mys:accounts-phone

For the sake of debugging we gonna write an authentication settings file (api/private/settings.json) which might make our life easier, but once your'e in production mode you shouldn't use this configuration:

5.2 Added settings file api/private/settings.json
1
2
3
4
5
6
7
8
{
  "accounts-phone": {
    "verificationWaitTime": 0,
    "verificationRetriesWaitTime": 0,
    "adminPhoneNumbers": ["+9721234567", "+97212345678", "+97212345679"],
    "phoneVerificationMasterCode": "1234"
  }
}

Now anytime we run our app we should provide it with a settings.json:

$ meteor run --settings private/settings.json

To make it simpler we can add start script to package.json:

5.3 Updated NPM script api/package.json
2
3
4
5
6
7
8
  "name": "api",
  "private": true,
  "scripts": {
    "start": "meteor run --settings private/settings.json"
  },
  "dependencies": {
    "babel-runtime": "^6.18.0",

NOTE: If you would like to test the verification with a real phone number, accounts-phone provides an easy access for twilio's API, for more information see accounts-phone's repo.

We will now apply the settings file we've just created so it can actually take effect:

5.4 Define SMS settings api/server/main.ts
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Chats, Messages } from "../collections/whatsapp-collections";
import * as moment from "moment";
import { initMethods } from "./methods";
import { Accounts } from 'meteor/accounts-base';
 
declare let SMS, Object;
 
Meteor.startup(() => {
  initMethods();
 
  if (Meteor.settings) {
    Object.assign(Accounts._options, Meteor.settings['accounts-phone']);
    SMS.twilio = Meteor.settings['twilio'];
  }
 
  if (Chats.find({}).cursor.count() === 0) {
    let chatId;
 

Meteor Accounts (Client Side)

And second, we will update the client, and add the corresponding authentication packages to it as well (run in the root directory):

$ npm install accounts-base-client-side --save
$ npm install accounts-phone --save

Let's import these packages in the app's main component so they can be a part of our bundle:

5.5 Added accounts packages to client side package.json
25
26
27
28
29
30
31
32
    "@ionic/storage": "1.1.6",
    "ionic-angular": "2.0.0-rc.3",
    "ionic-native": "2.2.3",
    "accounts-base-client-side": "^0.1.1",
    "accounts-phone": "0.0.1",
    "angular2-moment": "^1.0.0-beta.6",
    "ionicons": "3.0.0",
    "moment": "^2.15.2",

UI

For authentication we gonna create the following flow in our app:

  • login - The initial page. Ask for the user's phone number.
  • verification - Verify a user's phone number by an SMS authentication.
  • profile - Ask a user to pickup its name. Afterwards he will be promoted to the tabs page.

Before we implement these page, we need to identify if a user is currently logged in. If so, he will be automatically promoted to the chats view, if not, he is gonna be promoted to the login view and enter a phone number.

Let's apply this feature to our app's NgModule bootstrap:

5.7 Wait for user if logging in src/app/main.dev.ts
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'accounts-phone';
 
import { platformBrowserDynamic } from [email protected]/platform-browser-dynamic';
import { AppModule } from './app.module';
import { MeteorObservable } from 'meteor-rxjs';
 
declare let Meteor;
 
Meteor.startup(() => {
  const sub = MeteorObservable.autorun().subscribe(() => {
    if (Meteor.loggingIn()) return;
 
    setTimeout(() => {
      sub.unsubscribe();
    });
 
    platformBrowserDynamic().bootstrapModule(AppModule);
  });
});

And to production mode's entry point as well:

5.8 Wait for user if logging in, production src/app/main.prod.ts
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
import { platformBrowser } from [email protected]/platform-browser';
import { enableProdMode } from [email protected]/core';
import { AppModuleNgFactory } from './app.module.ngfactory';
import { MeteorObservable } from 'meteor-rxjs';
 
declare let Meteor;
 
Meteor.startup(() => {
  const sub = MeteorObservable.autorun().subscribe(() => {
    if (Meteor.loggingIn()) return;
 
    setTimeout(() => {
      sub.unsubscribe();
    });
 
    enableProdMode();
    platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
  });
});
 

Great, now that we're set, let's start implementing the views we mentioned earlier.

Let's start by creating the LoginComponent:

In this component we will request an SMS verification right after a phone number has been entered:

5.9 Create login component src/pages/auth/login.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
import { Component } from [email protected]/core';
import { NavController, AlertController } from 'ionic-angular';
 
declare let Accounts;
 
@Component({
  selector: 'login',
  templateUrl: "login.html"
})
export class LoginComponent {
  phone = '';
 
  constructor(
    public navCtrl: NavController,
    public alertCtrl: AlertController
  ) {}
 
  onInputKeypress({keyCode}: KeyboardEvent): void {
    if (keyCode == 13) {
      this.login();
    }
  }
 
  login(): void {
    const alert = this.alertCtrl.create({
      title: 'Confirm',
      message: `Would you like to proceed with the phone number ${this.phone}?`,
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel'
        },
        {
          text: 'Yes',
          handler: () => {
            this.handleLogin(alert);
            return false;
          }
        }
      ]
    });
 
    alert.present();
  }
 
  private handleLogin(alert): void {
    Accounts.requestPhoneVerification(this.phone, (e: Error) => {
      alert.dismiss().then(() => {
        if (e) return this.handleError(e);
 
        // this.navCtrl.push(VerificationComponent, {
        //   phone: this.phone
        // });
      });
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

Few things to be explained:

  • onInputKeypress is to catch Enter key press
  • login method creates an alert (see documentation) to confirm the action
  • handleError creates an alert with an error message
  • handleLogin calls Accounts.requestPhoneVerification request an SMS verification and moves to verification view.

Okay, the logic is clear. Let's move to the template:

5.10 Create login component view src/pages/auth/login.html
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
<ion-header>
  <ion-navbar color="whatsapp">
    <ion-title>Login</ion-title>
 
    <ion-buttons end>
      <button ion-button class="done-button" (click)="login()">Done</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
 
<ion-content padding class="login-page-content">
  <div class="instructions">
    <div>
      Please enter your phone number including its country code.
    </div>
    <br>
    <div>
      The messenger will send a one time SMS message to verify your phone number. Carrier SMS charges may apply.
    </div>
  </div>
 
  <ion-item>
    <ion-input [(ngModel)]="phone" (keypress)="onInputKeypress($event)" type="tel" placeholder="Your phone number"></ion-input>
  </ion-item>
</ion-content>

And let's add some styles:

5.11 Add login component view style src/pages/auth/login.scss
1
2
3
4
5
6
7
8
9
10
11
.login-page-content {
  .instructions {
    text-align: center;
    font-size: medium;
    margin: 50px;
  }
 
  .text-input {
    text-align: center;
  }
}

And add the Login Component to the NgModule definition:

5.12 Add login component to NgModule src/app/app.module.ts
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
24
25
26
27
28
29
30
31
import { ChatsPage } from "../pages/chats/chats";
import { MomentModule } from "angular2-moment";
import { MessagesPage } from "../pages/messages/messages";
import { LoginComponent } from "../pages/auth/login";
 
@NgModule({
  declarations: [
    MyApp,
    ChatsPage,
    TabsPage,
    MessagesPage,
    LoginComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    MyApp,
    ChatsPage,
    TabsPage,
    MessagesPage,
    LoginComponent
  ],
  providers: []
})

Now let's add the ability to identify whih page should be loaded - the main or login:

5.13 Add user identifiation in app's main component src/app/app.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component } from [email protected]/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { TabsPage } from '../pages/tabs/tabs';
import { LoginComponent } from '../pages/auth/login';
 
declare let Meteor;
 
@Component({
  template: `<ion-nav [root]="rootPage"></ion-nav>`
})
export class MyApp {
  rootPage: any;
 
  constructor(platform: Platform) {
    this.rootPage = Meteor.user() ? TabsPage : LoginComponent;
 
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.

That's great, everything is set up. We can now move to verification page.

Let's create a component called VerificationComponent:

5.14 Add verification component src/pages/verification/verification.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
import { Component, OnInit, NgZone } from [email protected]/core';
import { NavController, NavParams, AlertController } from 'ionic-angular';
 
declare let Accounts;
 
@Component({
  selector: 'verification',
  templateUrl: 'verification.html'
})
export class VerificationComponent implements OnInit {
  code: string = '';
  phone: string;
 
  constructor(
    public navCtrl: NavController,
    public alertCtrl: AlertController,
    public zone: NgZone,
    public navParams: NavParams
  ) {}
 
  ngOnInit() {
    this.phone = this.navParams.get('phone');
  }
 
  onInputKeypress({keyCode}: KeyboardEvent): void {
    if (keyCode == 13) {
      this.verify();
    }
  }
 
  verify(): void {
    Accounts.verifyPhone(this.phone, this.code, (e: Error) => {
      this.zone.run(() => {
        if (e) return this.handleError(e);
 
        // this.navCtrl.setRoot(ProfileComponent, {}, {
        //   animate: true
        // });
      });
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

Logic is pretty much the same as in LoginComponent. When verification succeed we redirect user to the ProfileComponent (this code is in comment, we will later remove the comment, after we add the actual component):

So let's add the view and the styles:

5.15 Add verification component view src/pages/verification/verification.html
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
<ion-header>
  <ion-navbar color="whatsapp">
    <ion-title>Verification</ion-title>
 
    <ion-buttons end>
      <button ion-button class="verify-button" (click)="verify()">Verify</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
 
<ion-content padding class="verification-page-content">
  <div class="instructions">
    <div>
      An SMS message with the verification code has been sent to {{phone}}.
    </div>
    <br>
    <div>
      To proceed, please enter the 4-digit verification code below.
    </div>
  </div>
 
  <ion-item>
    <ion-input [(ngModel)]="code" (keypress)="onInputKeypress($event)" type="tel" placeholder="Your verification code"></ion-input>
  </ion-item>
</ion-content>
5.16 Add verification component styles src/pages/verification/verification.scss
1
2
3
4
5
6
7
8
9
10
11
.verification-page-content {
  .instructions {
    text-align: center;
    font-size: medium;
    margin: 50px;
  }
 
  .text-input {
    text-align: center;
  }
}

And add it to the NgModule:

5.17 Add VerificationComponent to NgModule src/app/app.module.ts
6
7
8
9
10
11
12
 
14
15
16
17
18
19
20
21
 
27
28
29
30
31
32
33
34
import { MomentModule } from "angular2-moment";
import { MessagesPage } from "../pages/messages/messages";
import { LoginComponent } from "../pages/auth/login";
import { VerificationComponent } from "../pages/verification/verification";
 
@NgModule({
  declarations: [
...some lines skipped...
    ChatsPage,
    TabsPage,
    MessagesPage,
    LoginComponent,
    VerificationComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    ChatsPage,
    TabsPage,
    MessagesPage,
    LoginComponent,
    VerificationComponent
  ],
  providers: []
})

And now that we have the VerificationComponent we can use it inside the LoginComponent:

5.18 Add navigation to verification page from the login component src/pages/auth/login.ts
1
2
3
4
5
6
 
49
50
51
52
53
54
55
56
57
import { Component } from [email protected]/core';
import { NavController, AlertController } from 'ionic-angular';
import { VerificationComponent } from "../verification/verification";
 
declare let Accounts;
 
...some lines skipped...
      alert.dismiss().then(() => {
        if (e) return this.handleError(e);
 
        this.navCtrl.push(VerificationComponent, {
          phone: this.phone
        });
      });
    });
  }

Last step of our authentication pattern is to pickup a name.

Let's add a Profile interface, we will use it soon:

5.19 Add profile model decleration api/models/whatsapp-models.d.ts
1
2
3
4
5
6
7
8
9
declare module 'api/models/whatsapp-models' {
  interface Profile {
    name?: string;
    picture?: string;
  }
 
  interface Chat {
    _id?: string;
    title?: string;

And let's create the ProfileComponent:

5.20 Add profile component src/pages/profile/profile.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 { Component, OnInit } from [email protected]/core';
import { NavController, AlertController } from 'ionic-angular';
import { MeteorObservable } from 'meteor-rxjs';
import { Profile } from 'api/models/whatsapp-models';
import { TabsPage } from "../tabs/tabs";
 
declare let Meteor;
 
@Component({
  selector: 'profile',
  templateUrl: 'profile.html'
})
export class ProfileComponent implements OnInit {
  profile: Profile;
 
  constructor(
    public navCtrl: NavController,
    public alertCtrl: AlertController
  ) {}
 
  ngOnInit(): void {
    this.profile = Meteor.user().profile || {
      name: '',
      picture: '/ionicons/dist/svg/ios-contact.svg'
    };
  }
 
  done(): void {
    MeteorObservable.call('updateProfile', this.profile).subscribe({
      next: () => {
        this.navCtrl.push(TabsPage);
      },
      error: (e: Error) => {
        this.handleError(e);
      }
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

The logic is simple, call updateProfile (we will implement it soon!) and redirect to TabsPage which is our main view if the action succeed.

If you'll take a look at the constructor's logic we set the default profile picture to be one of ionicon's svgs. We need to make sure there is an access point available through the network to that asset. If we'd like to serve files as-is we simply gonna add them to the www dir. But first we'll need to update our .gitignore file to contain the upcoming changes:

5.21 Add ionicons to .gitignore .gitignore
26
27
28
29
30
31
32
33
plugins/
plugins/android.json
plugins/ios.json
www/*
!www/ionicons
$RECYCLE.BIN/
 
.DS_Store

And now that git can recognize our changes, let's add a symlink to ionicons in the www dir:

5.22 Add symlink to ionicons www/ionicons
1
../node_modules/ionicons

There's no component without a view:

5.23 Add profile component view src/pages/profile/profile.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ion-header>
  <ion-navbar color="whatsapp">
    <ion-title>Profile</ion-title>
 
    <ion-buttons end>
      <button ion-button class="done-button" (click)="done()">Done</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
 
<ion-content class="profile-page-content">
  <div class="profile-picture">
    <img [src]="profile.picture">
    <ion-icon name="create"></ion-icon>
  </div>
 
  <ion-item class="profile-name">
    <ion-label stacked>Name</ion-label>
    <ion-input [(ngModel)]="profile.name" placeholder="Your name"></ion-input>
  </ion-item>
</ion-content>

And styles:

5.24 Add profile component view style src/pages/profile/profile.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.profile-page-content {
  .profile-picture {
    max-width: 300px;
    display: block;
    margin: auto;
 
    img {
      margin-bottom: -33px;
      width: 100%;
    }
 
    ion-icon {
      float: right;
      font-size: 30px;
      opacity: 0.5;
      border-left: black solid 1px;
      padding-left: 5px;
    }
  }
}

And add it to the NgModule:

5.25 Add profile component to NgModule src/app/app.module.ts
7
8
9
10
11
12
13
 
16
17
18
19
20
21
22
23
 
30
31
32
33
34
35
36
37
import { MessagesPage } from "../pages/messages/messages";
import { LoginComponent } from "../pages/auth/login";
import { VerificationComponent } from "../pages/verification/verification";
import { ProfileComponent } from "../pages/profile/profile";
 
@NgModule({
  declarations: [
...some lines skipped...
    TabsPage,
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    TabsPage,
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent
  ],
  providers: []
})

And let's use it in the VerificationComponent:

5.26 Add profile component to verification navigation src/pages/verification/verification.ts
1
2
3
4
5
6
 
34
35
36
37
38
39
40
41
42
import { Component, OnInit, NgZone } from [email protected]/core';
import { NavController, NavParams, AlertController } from 'ionic-angular';
import { ProfileComponent } from "../profile/profile";
 
declare let Accounts;
 
...some lines skipped...
      this.zone.run(() => {
        if (e) return this.handleError(e);
 
        this.navCtrl.setRoot(ProfileComponent, {}, {
          animate: true
        });
      });
    });
  }

Our authentication flow is complete! However there are some few adjustments we need to make before we proceed to the next step.

For the messaging system, each message should have an owner. If a user is logged-in a message document should be inserted with an additional senderId field:

5.27 Add senderId property to addMessage method api/server/methods.ts
10
11
12
13
14
15
16
17
18
 
23
24
25
26
27
28
29
export function initMethods() {
  Meteor.methods({
    addMessage(chatId: string, content: string) {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to create a new chat');
 
      check(chatId, nonEmptyString);
      check(content, nonEmptyString);
 
...some lines skipped...
 
      return {
        messageId: Messages.collection.insert({
          senderId: this.userId,
          chatId: chatId,
          content: content,
          createdAt: new Date()
5.28 Add it also to the model api/models/whatsapp-models.d.ts
17
18
19
20
21
22
    content?: string;
    createdAt?: Date;
    ownership?: string;
    senderId?: string;
  }
}

We can determine message ownership inside the component:

5.29 Determine message ownership based on sender id src/pages/messages/messages.ts
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 
54
55
56
57
58
59
60
61
62
63
64
65
  messages: Observable<Message[]>;
  message: string = "";
  autoScroller: Subscription;
  senderId: string;
 
  constructor(navParams: NavParams, element: ElementRef) {
    this.selectedChat = <Chat>navParams.get('chat');
    this.title = this.selectedChat.title;
    this.picture = this.selectedChat.picture;
    this.senderId = Meteor.userId();
 
  }
 
  private get messagesPageContent(): Element {
...some lines skipped...
  }
 
  ngOnInit() {
    this.messages = Messages.find(
      {chatId: this.selectedChat._id},
      {sort: {createdAt: 1}}
    ).map((messages: Message[]) => {
      messages.forEach((message: Message) => {
        message.ownership = this.senderId == message.senderId ? 'mine' : 'other';
      });
 
      return messages;

Now we're going to add the abilities to log-out and edit our profile as well, which are going to be presented to us using a popover.

Let's show a popover any time we press on the options icon in the top right corner of the chats view!

Let's start by adding the actual Component that will open on the popover:

5.30 Add chats-options component src/pages/chat-options/chat-options.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
import { Component } from [email protected]/core';
import { NavController, ViewController, AlertController } from 'ionic-angular';
import { ProfileComponent } from '../profile/profile';
import { LoginComponent } from '../auth/login';
 
declare let Meteor;
 
@Component({
  selector: 'chats-options',
  templateUrl: 'chat-options.html'
})
export class ChatsOptionsComponent {
  constructor(
    public navCtrl: NavController,
    public viewCtrl: ViewController,
    public alertCtrl: AlertController
  ) {}
 
  editProfile(): void {
    this.viewCtrl.dismiss().then(() => {
      this.navCtrl.push(ProfileComponent);
    });
  }
 
  logout(): void {
    const alert = this.alertCtrl.create({
      title: 'Logout',
      message: 'Are you sure you would like to proceed?',
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel'
        },
        {
          text: 'Yes',
          handler: () => {
            this.handleLogout(alert);
            return false;
          }
        }
      ]
    });
 
    this.viewCtrl.dismiss().then(() => {
      alert.present();
    });
  }
 
  private handleLogout(alert): void {
    Meteor.logout((e: Error) => {
      alert.dismiss().then(() => {
        if (e) return this.handleError(e);
 
        this.navCtrl.setRoot(LoginComponent, {}, {
          animate: true
        });
      });
    });
  }
 
  private handleError(e: Error): void {
    console.error(e);
 
    const alert = this.alertCtrl.create({
      title: 'Oops!',
      message: e.message,
      buttons: ['OK']
    });
 
    alert.present();
  }
}

It uses popover functionality from Ionic (see documentation).

5.31 Add chats-options component view src/pages/chat-options/chat-options.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<ion-content class="chats-options-page-content">
  <ion-list class="options">
    <button ion-item class="option option-profile" (click)="editProfile()">
      <ion-icon name="contact" class="option-icon"></ion-icon>
      <div class="option-name">Profile</div>
    </button>
 
    <button ion-item class="option option-about">
      <ion-icon name="information-circle" class="option-icon"></ion-icon>
      <div class="option-name">About</div>
    </button>
 
    <button ion-item class="option option-logout" (click)="logout()">
      <ion-icon name="log-out" class="option-icon"></ion-icon>
      <div class="option-name">Logout</div>
    </button>
  </ion-list>
</ion-content>
5.32 Add chats-options component view style src/pages/chat-options/chat-options.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
.chats-options-page-content {
  .options {
    margin: 0;
  }
 
  .option-name {
    float: left;
  }
 
  .option-icon {
    float: right;
  }
}

And add it to the NgModule:

5.33 Add chats-options component to the module definition src/app/app.module.ts
8
9
10
11
12
13
14
 
18
19
20
21
22
23
24
25
 
33
34
35
36
37
38
39
40
import { LoginComponent } from "../pages/auth/login";
import { VerificationComponent } from "../pages/verification/verification";
import { ProfileComponent } from "../pages/profile/profile";
import { ChatsOptionsComponent } from "../pages/chat-options/chat-options";
 
@NgModule({
  declarations: [
...some lines skipped...
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent
  ],
  imports: [
    IonicModule.forRoot(MyApp),
...some lines skipped...
    MessagesPage,
    LoginComponent,
    VerificationComponent,
    ProfileComponent,
    ChatsOptionsComponent
  ],
  providers: []
})

Now let's use it inside the ChatsPage:

5.34 Use the popover component src/pages/chats/chats.ts
2
3
4
5
6
7
8
9
10
 
12
13
14
15
16
17
18
 
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { Observable } from "rxjs";
import { Chat } from "api/models/whatsapp-models";
import { Chats, Messages } from "api/collections/whatsapp-collections";
import { NavController, PopoverController } from "ionic-angular";
import { MessagesPage } from "../messages/messages";
import { ChatsOptionsComponent } from "../chat-options/chat-options";
 
@Component({
  templateUrl: 'chats.html'
...some lines skipped...
export class ChatsPage implements OnInit {
  chats;
 
  constructor(public navCtrl: NavController, public popoverCtrl: PopoverController) {
 
  }
 
...some lines skipped...
      ).zone();
  }
 
  showOptions(): void {
    const popover = this.popoverCtrl.create(ChatsOptionsComponent, {}, {
      cssClass: 'options-popover'
    });
 
    popover.present();
  }
 
  showMessages(chat): void {
    this.navCtrl.push(MessagesPage, {chat});
  }

And let's add an event handler in the view:

5.35 Added event handler src/pages/chats/chats.html
7
8
9
10
11
12
13
      <button ion-button icon-only class="add-chat-button">
        <ion-icon name="person-add"></ion-icon>
      </button>
      <button ion-button icon-only class="options-button" (click)="showOptions()">
        <ion-icon name="more"></ion-icon>
      </button>
    </ion-buttons>

As for now, once you click on the options icon in the chats view, the popover should appear in the middle of the screen. To fix it, we simply gonna edit the scss file of the chats page:

5.36 Add options-popover style to chats stylesheet src/pages/chats/chats.scss
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    }
  }
}
 
.options-popover {
  $popover-width: 200px;
  $popover-margin: 5px;
 
  .popover-content {
    width: $popover-width;
    transform-origin: right top 0px !important;
    left: calc(100% - #{$popover-width} - #{$popover-margin}) !important;
    top: $popover-margin !important;
  }
}

And last, let's implement the server side method (updateProfile):

5.37 Added updateProfile method api/server/methods.ts
1
2
3
4
5
6
7
 
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Meteor } from 'meteor/meteor';
import { Chats, Messages } from "../collections/whatsapp-collections";
import { check, Match } from "meteor/check";
import { Profile } from "api/models/whatsapp-models";
 
const nonEmptyString = Match.Where((str) => {
  check(str, String);
...some lines skipped...
 
export function initMethods() {
  Meteor.methods({
    updateProfile(profile: Profile): void {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to create a new chat');
 
      check(profile, {
        name: nonEmptyString,
        picture: nonEmptyString
      });
 
      Meteor.users.update(this.userId, {
        $set: {profile}
      });
    },
    addMessage(chatId: string, content: string) {
      if (!this.userId) throw new Meteor.Error('unauthorized',
        'User must be logged-in to create a new chat');