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:
api$ meteor add accounts-base
api$ meteor add npm-bcrypt
api$ 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:
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
:
4
5
6
7
8
9
10
"homepage": "http://ionicframework.com/",
"private": true,
"scripts": {
"api": "cd api && meteor run --settings private/settings.json",
"ionic:build": "ionic-app-scripts build",
"ionic:serve": "ionic-app-scripts serve"
},
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as moment from "moment";
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { initMethods } from "./methods";
import { Chats, Messages } from "../collections/whatsapp-collections";
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;
Second, we will update the client, and add the corresponding authentication packages to it as well:
$ npm install --save accounts-base-client-side
$ npm install --save accounts-phone
Let's import these packages in the app's main component so they can be a part of our bundle:
21
22
23
24
25
26
27
28
"@ionic/storage": "1.1.6",
"@types/meteor": "^1.3.31",
"@types/underscore": "^1.7.36",
"accounts-base-client-side": "^0.1.1",
"accounts-phone": "0.0.1",
"angular2-moment": "^1.0.0",
"babel-runtime": "^6.20.0",
"ionic-angular": "2.0.0-rc.4",
Install the necessary typings:
$ npm install --save @types/meteor-accounts-phone
And import them:
20
21
22
23
24
25
26
"@angular/platform-server": "2.2.1",
"@ionic/storage": "1.1.6",
"@types/meteor": "^1.3.31",
"@types/meteor-accounts-phone": "0.0.5",
"@types/underscore": "^1.7.36",
"accounts-base-client-side": "^0.1.1",
"accounts-phone": "0.0.1",
12
13
14
15
16
17
18
https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html
*/
/// <reference types="meteor-typings" />
/// <reference types="@types/meteor-accounts-phone" />
/// <reference types="@types/underscore" />
/// <reference path="../api/models/whatsapp-models.d.ts" />
declare module '*';
For authentication we gonna create the following flow in our app:
Before we implement these pages, 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 entry script:
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'accounts-phone';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
import { MeteorObservable } from 'meteor-rxjs';
Meteor.startup(() => {
const sub = MeteorObservable.autorun().subscribe(() => {
if (Meteor.loggingIn()) return;
setTimeout(() => {
sub.unsubscribe();
});
platformBrowserDynamic().bootstrapModule(AppModule);
});
});
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:
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
import { Component } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
@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();
}
}
The onInputKeypress
handler is used to detect key press events. Once we press the login button, the login
method is called and shows and alert dialog to confirm the action (See reference). If an error has occurred, the handlerError
method is called and shows an alert dialog with the received error. If everything went as expected the handleLogin
method is called. It requests for an SMS verification using Accounts.requestPhoneVerification
, and promotes us to the verification view.
Hopefully that the component's logic is clear now, let's move to the template:
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 add some style into it:
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;
}
}
As usual, newly created components should be imported in the app's module:
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: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
Now let's add the ability to identify which page should be loaded - the chats page or the login page:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { TabsPage } from '../pages/tabs/tabs';
import { LoginComponent } from '../pages/auth/login';
@Component({
templateUrl: 'app.html'
})
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.
Let's proceed and implement the verification page. We will start by creating its component, called VerificationComponent
:
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
import { Component, OnInit, NgZone } from '@angular/core';
import { NavController, NavParams, AlertController } from 'ionic-angular';
import { Accounts } from 'meteor/accounts-base';
@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 the login component. When verification succeeds we redirect the user to the ProfileComponent
. Let's add the view template and the stylesheet:
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>
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:
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: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
And now that we have the VerificationComponent
we can use it inside the LoginComponent
:
1
2
3
4
5
6
47
48
49
50
51
52
53
54
55
import { Component } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
import { VerificationComponent } from "../verification/verification";
@Component({
selector: 'login',
...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. We will create a Profile
interface so the compiler can recognize profile-data structures:
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
:
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
import { Component, OnInit } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
import { MeteorObservable } from 'meteor-rxjs';
import { Profile } from 'api/models/whatsapp-models';
import { TabsPage } from "../tabs/tabs";
@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. We call the updateProfile
method and redirect the user to the TabsPage
if the action succeeded. The updateProfile
method should look like so:
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) {
check(chatId, nonEmptyString);
check(content, nonEmptyString);
If you'll take a look at the constructor's logic of the ProfileComponent
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:
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:
www$ ln -s ../node_modules/ionicons
Now we can implement the view and the stylesheet:
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>
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;
}
}
}
Import our newly created component:
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: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
And use it in the VerificationComponent
:
1
2
3
4
5
6
7
33
34
35
36
37
38
39
40
41
import { Component, OnInit, NgZone } from '@angular/core';
import { NavController, NavParams, AlertController } from 'ionic-angular';
import { Accounts } from 'meteor/accounts-base';
import { ProfileComponent } from "../profile/profile";
@Component({
selector: 'verification',
...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:
24
25
26
27
28
29
30
31
32
37
38
39
40
41
42
43
});
},
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()
17
18
19
20
21
22
content?: string;
createdAt?: Date;
ownership?: string;
senderId?: string;
}
}
We can determine message ownership inside the component:
16
17
18
19
20
21
22
23
24
25
26
27
28
29
47
48
49
50
51
52
53
54
55
56
57
58
messages: Observable<Message[]>;
message: string = "";
autoScroller: MutationObserver;
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.
A popover, just like a page in our app, consists of a component, view, and style:
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
import { Component } from '@angular/core';
import { NavController, ViewController, AlertController } from 'ionic-angular';
import { ProfileComponent } from '../profile/profile';
import { LoginComponent } from '../auth/login';
@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();
}
}
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>
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;
}
}
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: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
Now let's use it inside the ChatsPage
:
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 which will show the popover:
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:
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;
}
}
{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/messages-page" next_ref="https://angular-meteor.com/tutorials/whatsapp2/ionic/1.0.0/chats-mutations"}}}