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.
Firt 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:
$ meteor add npm-bcrypt
$ meteor add accounts-base
$ meteor add mys:accounts-phone
We will also need to install their decleration files so Typescript know how to handle them:
$ typings install dt~meteor-accounts-phone --save --global
For the sake of debugging we gonna write an authentication settings file (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
:
2
3
4
5
6
7
8
"name": "angular2-meteor-base",
"private": true,
"scripts": {
"start": "meteor run --settings private/settings.json",
"test": "meteor test --driver-package practicalmeteor:mocha",
"test:ci": "meteor test --once --driver-package dispatch:mocha-phantomjs",
"postinstall": "typings i"
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
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
if (Meteor.settings) {
Object.assign(Accounts._options, Meteor.settings['accounts-phone']);
SMS.twilio = Meteor.settings['twilio'];
}
We created server/imports/api/sms.ts
file, now we have to import it inside main.ts
:
1
2
3
4
5
6
import { Main } from './imports/server-main/main';
import './imports/methods/methods';
import './imports/api/sms';
const mainInstance = new Main();
mainInstance.start();
For authentication we're going to create the following flow in our app:
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 going to be promoted to the login view and enter a phone number.
Let's apply this feature to our app's main component:
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { Meteor } from "meteor/meteor";
import { MeteorObservable } from 'meteor-rxjs';
import { AppModule } from './imports/app/app.module';
enableProdMode();
Meteor.startup(() => {
const sub = MeteorObservable.autorun().subscribe(() => {
if (Meteor.loggingIn()) return;
setTimeout(() => {
sub.unsubscribe();
});
platformBrowserDynamic().bootstrapModule(AppModule);
});
});
We don't have yet a proper component with auth logic but let's add it anyway as LoginComponent
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component } from '@angular/core';
import { Platform } from "ionic-angular";
import { StatusBar } from "ionic-native";
import { Meteor } from 'meteor/meteor';
import template from './app.component.html';
import {TabsContainerComponent} from "../pages/tabs-container/tabs-container.component";
import {LoginComponent} from '../pages/auth/login.component';
@Component({
selector: 'app',
template
})
export class AppComponent {
rootPage: any;
constructor(platform: Platform) {
this.rootPage = Meteor.user() ? TabsContainerComponent : 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.
Great, now that we're set, let's start implementing the views we mentioned earlier. We will start with the login component.
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
68
69
70
71
72
73
74
import { Component } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
import { Accounts } from 'meteor/accounts-base';
import { VerificationComponent } from './verification.component';
import template from './login.component.html';
import style from "./login.component.scss";
@Component({
selector: 'login',
template,
styles: [
style
]
})
export class LoginComponent {
phone = '';
constructor(
private navCtrl: NavController,
private 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 presslogin
method creates an alert (see documentation) to confirm the actionhandleError
creates an alert with an error messagehandleLogin
calls Accounts.requestPhoneVerification
request an SMS verification and moves to verification view.Okay, the logic is clear. 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 styles:
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;
}
}
Our new component is not available yet:
5
6
7
8
9
10
11
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {ChatsComponent} from "../pages/chats/chats.component";
import {MomentModule} from "angular2-moment";
import {MessagesPage} from "../pages/chat/messages-page.component";
import {LoginComponent} from '../pages/auth/login.component';
@NgModule({
// Components, Pipes, Directive
...some lines skipped...
AppComponent,
TabsContainerComponent,
ChatsComponent,
MessagesPage,
LoginComponent
],
// Entry Components
entryComponents: [
AppComponent,
TabsContainerComponent,
ChatsComponent,
MessagesPage,
LoginComponent
],
// Providers
providers: [
That's great, everything is set up. We can now move to verification page.
Let's create a 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
54
55
56
57
58
59
import {Component, OnInit, NgZone} from '@angular/core';
import {NavController, NavParams, AlertController} from 'ionic-angular';
import {Accounts} from 'meteor/accounts-base';
import {ProfileComponent} from './profile.component';
import template from './verification.component.html';
import style from './verification.component.scss';
@Component({
selector: 'verification',
template,
styles: [
style
]
})
export class VerificationComponent implements OnInit {
code: string = '';
phone: string;
constructor(
private navCtrl: NavController,
private alertCtrl: AlertController,
private zone: NgZone,
private 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
.
So let's add the view and the styles:
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;
}
}
Make it available to AppModule:
6
7
8
9
10
11
12
15
16
17
18
19
20
21
22
24
25
26
27
28
29
30
31
import {MomentModule} from "angular2-moment";
import {MessagesPage} from "../pages/chat/messages-page.component";
import {LoginComponent} from '../pages/auth/login.component';
import {VerificationComponent} from '../pages/auth/verification.component';
@NgModule({
// Components, Pipes, Directive
...some lines skipped...
TabsContainerComponent,
ChatsComponent,
MessagesPage,
LoginComponent,
VerificationComponent
],
// Entry Components
entryComponents: [
...some lines skipped...
TabsContainerComponent,
ChatsComponent,
MessagesPage,
LoginComponent,
VerificationComponent
],
// Providers
providers: [
Last step of our authentication pattern is to pickup a name.
Let's add a Method that updates user's profile:
2
3
4
5
6
7
8
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {Chats} from "../../../both/collections/chats.collection";
import {Messages} from "../../../both/collections/messages.collection";
import {check, Match} from 'meteor/check';
import {Profile} from '../../../both/models/profile.model';
const nonEmptyString = Match.Where((str) => {
check(str, String);
...some lines skipped...
});
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): void {
check(chatId, nonEmptyString);
check(content, nonEmptyString);
It would be nice to define a separate model for a profile:
1
2
3
4
export interface Profile {
name?: string;
picture?: string;
}
Now we can 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
49
50
51
52
53
54
55
import {Component, OnInit} from '@angular/core';
import {NavController, AlertController} from 'ionic-angular';
import {Meteor} from 'meteor/meteor';
import {MeteorObservable} from 'meteor-rxjs';
import {Profile} from '../../../../both/models/profile.model';
import {TabsContainerComponent} from '../tabs-container/tabs-container.component';
import template from './profile.component.html';
import style from './profile.component.scss';
@Component({
selector: 'profile',
template,
styles: [
style
]
})
export class ProfileComponent implements OnInit {
profile: Profile;
constructor(
private navCtrl: NavController,
private alertCtrl: AlertController
) {}
ngOnInit(): void {
this.profile = Meteor.user().profile || {
name: '',
picture: '/assets/ionicons/dist/svg/ios-contact.svg'
};
}
done(): void {
MeteorObservable.call('updateProfile', this.profile).subscribe({
next: () => {
this.navCtrl.push(TabsContainerComponent);
},
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
and redirect to TabsContainerComponent
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 assets
dir; So let's add a symlink to ionicons
in that dir:
public/assets$ ln -s ../../node_modules/ionicons
There's no component without a view:
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>
There's no good looking view without a stylesheet:
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;
}
}
}
There's no access to the component without a declaration inside AppModule:
7
8
9
10
11
12
13
17
18
19
20
21
22
23
24
27
28
29
30
31
32
33
34
import {MessagesPage} from "../pages/chat/messages-page.component";
import {LoginComponent} from '../pages/auth/login.component';
import {VerificationComponent} from '../pages/auth/verification.component';
import {ProfileComponent} from '../pages/auth/profile.component';
@NgModule({
// Components, Pipes, Directive
...some lines skipped...
ChatsComponent,
MessagesPage,
LoginComponent,
VerificationComponent,
ProfileComponent
],
// Entry Components
entryComponents: [
...some lines skipped...
ChatsComponent,
MessagesPage,
LoginComponent,
VerificationComponent,
ProfileComponent
],
// Providers
providers: [
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): void {
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...
Messages.collection.insert({
chatId: chatId,
senderId: this.userId,
content: content,
createdAt: new Date()
});
1
2
3
4
5
6
7
export interface Message {
_id?: string;
chatId?: string;
senderId?: string;
content?: string;
ownership?: string;
createdAt?: Date;
We can determine message ownership inside the component:
1
2
3
4
5
6
21
22
23
24
25
26
27
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import {Component, OnInit, OnDestroy} from "@angular/core";
import {NavParams} from "ionic-angular";
import {Meteor} from 'meteor/meteor';
import {Chat} from "../../../../both/models/chat.model";
import {Messages} from "../../../../both/collections/messages.collection";
import {Observable} from "rxjs";
...some lines skipped...
private title: string;
private picture: string;
private messages: Observable<Message[]>;
private senderId: string;
private message = "";
private autoScroller: MutationObserver;
...some lines skipped...
this.selectedChat = <Chat>navParams.get('chat');
this.title = this.selectedChat.title;
this.picture = this.selectedChat.picture;
this.senderId = Meteor.userId();
}
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:
7
8
9
10
11
12
13
14
15
21
22
23
24
25
26
27
28
29
30
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import {Chats} from "../../../../both/collections/chats.collection";
import {Message} from "../../../../both/models/message.model";
import {Messages} from "../../../../both/collections/messages.collection";
import {NavController, PopoverController} from "ionic-angular";
import {MessagesPage} from "../chat/messages-page.component";
import {ChatsOptionsComponent} from '../chats/chats-options.component';
@Component({
selector: "chats",
...some lines skipped...
export class ChatsComponent implements OnInit {
chats: Observable<Chat[]>;
constructor(
private navCtrl: NavController,
private popoverCtrl: PopoverController
) {}
ngOnInit() {
this.chats = Chats
...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});
}
4
5
6
7
8
9
10
<ion-buttons end>
<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>
</ion-navbar>
</ion-header>
It uses popover functionality from Ionic (see documentation).
As you can see, we used ChatsOptionsComponent
.
Now let's implement the component for the chats options which will handle the profile editing and logging-out:
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
import {Component} from '@angular/core';
import {NavController, ViewController, AlertController} from 'ionic-angular';
import {Meteor} from 'meteor/meteor';
import {ProfileComponent} from '../auth/profile.component';
import {LoginComponent} from '../auth/login.component';
import template from './chats-options.component.html';
import style from "./chats-options.component.scss";
@Component({
selector: 'chats-options',
template,
styles: [
style
]
})
export class ChatsOptionsComponent {
constructor(
private navCtrl: NavController,
private viewCtrl: ViewController,
private 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();
}
}
Add a template and styles:
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;
}
}
Add it to AppModule:
8
9
10
11
12
13
14
19
20
21
22
23
24
25
26
30
31
32
33
34
35
36
37
import {LoginComponent} from '../pages/auth/login.component';
import {VerificationComponent} from '../pages/auth/verification.component';
import {ProfileComponent} from '../pages/auth/profile.component';
import {ChatsOptionsComponent} from '../pages/chats/chats-options.component';
@NgModule({
// Components, Pipes, Directive
...some lines skipped...
MessagesPage,
LoginComponent,
VerificationComponent,
ProfileComponent,
ChatsOptionsComponent
],
// Entry Components
entryComponents: [
...some lines skipped...
MessagesPage,
LoginComponent,
VerificationComponent,
ProfileComponent,
ChatsOptionsComponent
],
// Providers
providers: [
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 add the following scss
file to the styles
dir:
1
2
3
4
5
6
7
8
9
10
11
.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;
}
}
Note that this style-sheet is not a component specific, so it has to be loaded as a global asset rather then provide it to the component during its initialization.
{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/messages-page" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/chats-mutations"}}}