Packages Isolation

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

This step of the tutorial teaches us how to add mobile support for iOS and Android and how to elegantly reuse code using the es2015 modules.

In this tutorial's example we will differentiate the login part of the project: in the browser users will login using email and password and in the mobile app users will login with SMS verification.

Adding mobile support for the project:

To add mobile support, select the platform(s) you want and run the following command:

$ meteor add-platform ios
# OR / AND
$ meteor add-platform android

And now to run in the emulator, run:

$ meteor run ios
# OR
$ meteor run android

You can also run in a real mobile device, for more instructions, read the "Mobile" chapter of the Official Meteor Guide.

Before we can run Meteor and Angular 2 on mobile platform, we need to make sure that our Angular 2 NgModule initialized only when Meteor platform declares that it's ready, to do so, let's wrap the bootstrapModule with Meteor.startup:

22.2 Wrapped Angular 2 bootstrap with Meteor startup client/main.ts
6
7
8
9
10
11
12
 
import '../both/methods/parties.methods';
 
Meteor.startup(() => {
  const platform = platformBrowserDynamic();
  platform.bootstrapModule(AppModule);
});

Creating Mobile/Web Separation

We're going to keep the view and the component for the web under *.component.web.html and *.component.web.ts and doing the same for *.component.mobile.html and *.component.mobile.ts.

First thing to do is to rename login.component.html to login.component.web.html:

22.3 Rename to login.web.component.html client/imports/app/auth/login.component.web.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
26
27
28
29
30
31
32
33
34
35
36
37
<div class="md-content" layout="row" layout-align="center start" layout-fill layout-margin>
  <div layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
    <md-toolbar class="md-primary" color="primary">
      Sign in
    </md-toolbar>
 
    <div layout="column" layout-margin layout-padding>
      <div layout="row" layout-margin>
        <p class="md-body-2"> Sign in with your email</p>
      </div>
 
      <form [formGroup]="loginForm" #f="ngForm" (ngSubmit)="login()"
            layout="column" layout-fill layout-padding layout-margin>
 
        <md-input formControlName="email" type="email" placeholder="Email"></md-input>
        <md-input formControlName="password" type="password" placeholder="Password"></md-input>
 
        <div layout="row" layout-align="space-between center">
          <a md-button [routerLink]="['/recover']">Forgot password?</a>
          <button md-raised-button class="md-primary" type="submit" aria-label="login">Sign In</button>
        </div>
      </form>
 
      <div [hidden]="error == ''">
        <md-toolbar class="md-warn" layout="row" layout-fill layout-padding layout-margin>
          <p class="md-body-1">{{ error }}</p>
        </md-toolbar>
      </div>
 
      <md-divider></md-divider>
 
      <div layout="row" layout-align="center">
        <a md-button [routerLink]="['/signup']">Need an account?</a>
      </div>
    </div>
  </div>
</div>

Let's do the same with login.component.ts file:

22.4 Do the same for login.component.ts client/imports/app/auth/login.component.web.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
import {Component, OnInit, NgZone} from [email protected]/core';
import { FormBuilder, FormGroup, Validators } from [email protected]/forms';
import { Router } from [email protected]/router';
import { Meteor } from 'meteor/meteor';
 
import template from './login.component.html';
 
@Component({
  selector: 'login',
  template
})
export class LoginComponent implements OnInit {
  loginForm: FormGroup;
  error: string;
 
  constructor(private router: Router, private zone: NgZone, private formBuilder: FormBuilder) {}
 
  ngOnInit() {
    this.loginForm = this.formBuilder.group({
      email: ['', Validators.required],
      password: ['', Validators.required]
    });
 
    this.error = '';
  }
 
  login() {
    if (this.loginForm.valid) {
      Meteor.loginWithPassword(this.loginForm.value.email, this.loginForm.value.password, (err) => {
        if (err) {
          this.zone.run(() => {
            this.error = err;
          });
        } else {
          this.router.navigate(['/']);
        }
      });
    }
  }
}

with one small change which is a new template property:

3
4
5
6
7
8
9
import { Router } from [email protected]/router';
import { Meteor } from 'meteor/meteor';
 
import template from './login.component.web.html';
 
@Component({
  selector: 'login',

And let's update the imports in the index file:

22.6 Update the index file client/imports/app/auth/index.ts
1
2
3
4
import {LoginComponent} from "./login.component.web";
import {SignupComponent} from "./signup.component";
import {RecoverComponent} from "./recover.component";
 

SMS verification

As I mentioned before, we're going to use SMS verification to log in a user on the mobile application.

There is a package for that!

We will use an external package that extends Meteor's Accounts, called accounts-phone that verifies phone number with SMS message, so let's add it:

$ meteor add mys:accounts-phone

Note that in development mode - the SMS will not be sent - and the verification code will be printed to the Meteor log.

We can now move on to create a mobile version Login Component.

A template of a mobile version will be pretty much the same as for browsers:

22.8 Create a view for the mobile login client/imports/app/auth/login.component.mobile.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div class="md-content" layout="row" layout-align="center start" layout-fill layout-margin>
  <div layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
    <md-toolbar class="md-primary" color="primary">
      Sign in
    </md-toolbar>
 
    <div layout="column" layout-fill layout-margin layout-padding>
      <div layout="row" layout-fill layout-margin>
        <p class="md-body-2"> Sign in with SMS</p>
      </div>
 
      <div [hidden]="error == ''">
        <md-toolbar class="md-warn" layout="row" layout-fill layout-padding layout-margin>
          <p class="md-body-1">{{ error }}</p>
        </md-toolbar>
      </div>
    </div>
  </div>
</div>

We can use the same directives in the component as in Web version, so let's create a basic component without any functionality:

22.9 Create mobile version of Login component client/imports/app/auth/login.component.mobile.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component, OnInit } from [email protected]/core';
import { FormBuilder, FormGroup, Validators } from [email protected]/forms';
import { Router } from [email protected]/router';
 
import template from './login.component.mobile.html';
 
@Component({
  selector: 'login',
  template
})
export class MobileLoginComponent implements OnInit {
  error: string = '';
 
  constructor(private router: Router, private formBuilder: FormBuilder) {}
 
  ngOnInit() {}
}

SMS verification is a two-step process. First thing to do is to verify a phone number.

Let's create a form for that:

22.10 Add a form with phone number to provide client/imports/app/auth/login.component.mobile.html
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
        <p class="md-body-2"> Sign in with SMS</p>
      </div>
 
      <form [formGroup]="phoneForm" #f="ngForm" (ngSubmit)="send()"
            layout="column" layout-fill layout-padding layout-margin>
 
        <md-input formControlName="phone" type="text" placeholder="Phone"></md-input>
 
        <div layout="row" layout-align="space-between center">
          <button md-raised-button class="md-primary" type="submit" aria-label="send">Send SMS</button>
        </div>
      </form>
 
      <div [hidden]="error == ''">
        <md-toolbar class="md-warn" layout="row" layout-fill layout-padding layout-margin>
          <p class="md-body-1">{{ error }}</p>

It's a simple form, basically the same as the form with Email and password verification we did in previous chapters.

We can now take care of the logic. Let's create a send method:

22.11 Implement a phone number verification client/imports/app/auth/login.component.mobile.ts
1
2
3
4
5
6
7
 
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
import {Component, OnInit, NgZone} from [email protected]/core';
import { FormBuilder, FormGroup, Validators } from [email protected]/forms';
import { Router } from [email protected]/router';
import { Accounts } from 'meteor/accounts-base';
 
import template from './login.component.mobile.html';
 
...some lines skipped...
})
export class MobileLoginComponent implements OnInit {
  error: string = '';
  phoneForm: FormGroup;
  phone: string;
 
  constructor(private router: Router, private zone: NgZone, private formBuilder: FormBuilder) {}
 
  ngOnInit() {
    this.phoneForm = this.formBuilder.group({
      phone: ['', Validators.required]
    });
  }
 
  send() {
    if (this.phoneForm.valid) {
      Accounts.requestPhoneVerification(this.phoneForm.value.phone, (err) => {
        this.zone.run(() => {
          if (err) {
            this.error = err.reason || err;
          } else {
            this.phone = this.phoneForm.value.phone;
            this.error = '';
          }
        });
      });
    }
  }
}

What we did? Few things:

  • form called phoneForm with one field phone.
  • send method that calls Accounts.requestPhoneVerification to verify phone number and to send SMS with verification code.
  • we're also keeping phone number outside the form's scope.

Great, we're half way there!

Now we need to verify that code. We will keep all the logic under verify method:

22.12 Implement a code verification client/imports/app/auth/login.component.mobile.ts
13
14
15
16
17
18
19
20
 
22
23
24
25
26
27
28
29
30
31
 
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  error: string = '';
  phoneForm: FormGroup;
  phone: string;
  verifyForm: FormGroup;
  isStepTwo: boolean = false;
 
  constructor(private router: Router, private zone: NgZone, private formBuilder: FormBuilder) {}
 
...some lines skipped...
    this.phoneForm = this.formBuilder.group({
      phone: ['', Validators.required]
    });
 
    this.verifyForm = this.formBuilder.group({
        code: ['', Validators.required]
    });
  }
 
  send() {
...some lines skipped...
          } else {
            this.phone = this.phoneForm.value.phone;
            this.error = '';
            this.isStepTwo = true;
          }
        });
      });
    }
  }
 
 
  verify() {
    if (this.verifyForm.valid) {
      Accounts.verifyPhone(this.phone, this.verifyForm.value.code, (err) => {
        this.zone.run(() => {
          if (err) {
            this.error = err.reason || err;
          }
          else {
            this.router.navigate(['/']);
          }
        });
      });

As you can see, we used Accounts.verifyPhone with proper arguments to call the verification process.

There are two more things that you should notice.

  • New property isStepTwo that holds the status of sign in process. Based on that property we can tell if someone is still in the first phase or he already wants to verify code sent via SMS.
  • Redirection to PartiesList if verification succeed.

We have all the logic, we still need to create a view for it:

22.13 Create a form with code verification client/imports/app/auth/login.component.mobile.html
9
10
11
12
13
14
15
 
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
        <p class="md-body-2"> Sign in with SMS</p>
      </div>
 
      <form [formGroup]="phoneForm" *ngIf="!isStepTwo" #f="ngForm" (ngSubmit)="send()"
            layout="column" layout-fill layout-padding layout-margin>
 
        <md-input formControlName="phone" type="text" placeholder="Phone"></md-input>
...some lines skipped...
        </div>
      </form>
 
      <form *ngIf="isStepTwo" [formGroup]="verifyForm" #f="ngForm" (ngSubmit)="verify()"
            layout="column" layout-fill layout-padding layout-margin>
 
        <md-input formControlName="code" type="text" placeholder="Code"></md-input>
 
        <div layout="row" layout-align="space-between center">
          <button md-raised-button class="md-primary" type="submit" aria-label="verify">Verify code</button>
        </div>
      </form>
 
      <div [hidden]="error == ''">
        <md-toolbar class="md-warn" layout="row" layout-fill layout-padding layout-margin>
          <p class="md-body-1">{{ error }}</p>

And let's add the mobile version of the Component to the index file:

22.14 Added MobileLoginComponent to the index file client/imports/app/auth/index.ts
1
2
3
4
5
6
7
8
9
10
11
import {LoginComponent} from "./login.component.web";
import {SignupComponent} from "./signup.component";
import {RecoverComponent} from "./recover.component";
import {MobileLoginComponent} from "./login.component.mobile";
 
export const AUTH_DECLARATIONS = [
  LoginComponent,
  SignupComponent,
  RecoverComponent,
  MobileLoginComponent
];

It seems like both versions are ready.

We can now move on to client/app.routes.ts.

Just as you can use Meteor.isServer and Meteor.isClient to separate your client-side and server-side code, you can use Meteor.isCordova to separate your Cordova-specific code from the rest of your code.

22.15 Choose mobile or web version client/imports/app/app.routes.ts
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
import { PartiesListComponent } from './parties/parties-list.component';
import { PartyDetailsComponent } from './parties/party-details.component';
import {SignupComponent} from "./auth/signup.component";
import {RecoverComponent} from "./auth/recover.component";
import {MobileLoginComponent} from "./auth/login.component.mobile";
import {LoginComponent} from "./auth/login.component.web";
 
export const routes: Route[] = [
  { path: '', component: PartiesListComponent },
  { path: 'party/:partyId', component: PartyDetailsComponent, canActivate: ['canActivateForLoggedIn'] },
  { path: 'login', component: Meteor.isCordova ? MobileLoginComponent : LoginComponent },
  { path: 'signup', component: SignupComponent },
  { path: 'recover', component: RecoverComponent }
];

As you can see, we're importing both version of Login Component. But only one is being used, depending on Meteor.isCordova value.

If we would run Socially in a browser LoginComponent for web platform will be used.

And that's it!

Summary

In this tutorial we showed how to make our code behave differently in mobile and web platforms. We did this by creating separate es2015 modules with specific code for mobile and web, and using them based on the platform that runs the application.