Fork me on GitHub

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.

Creating es2015 modules

We're going to keep the view and the controller for the web under web.html and web.js and doing the same for mobile.html and mobile.js.

First thing to do is to move the content of login.html to web.html file:

21.2 Move login.html to web.html imports/ui/components/login/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
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
<md-content layout="row" layout-align="center start" layout-fill layout-margin>
  <md-whiteframe layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
    <md-toolbar class="md-primary md-tall" layout="column" layout-align="end" layout-fill>
      <div layout="row" class="md-toolbar-tools md-toolbar-tools-bottom">
        <h3 class="md-display-1">
          Sign in
        </h3>
      </div>
    </md-toolbar>
 
    <div layout="column" layout-fill layout-margin layout-padding>
      <div layout="row" layout-fill layout-margin>
        <p class="md-body-2">
          Use existing account</p>
      </div>
 
      <div layout="row" layout-fill layout-margin layout-padding layout-wrap>
        <md-button class="md-raised">
          <md-icon md-svg-icon="social:ic_google_24px" style="color: #DC4A38;"></md-icon>
          <span>Google</span>
        </md-button>
        <md-button class="md-raised">
          <md-icon md-svg-icon="social:ic_facebook_24px" style="color: #3F62B4;"></md-icon>
          <span>Facebook</span>
        </md-button>
        <md-button class="md-raised">
          <md-icon md-svg-icon="social:ic_twitter_24px" style="color: #27AAE2;"></md-icon>
          <span>Twitter</span>
        </md-button>
      </div>
      <md-divider class="inset"></md-divider>
 
      <div layout="row" layout-fill layout-margin>
        <p class="md-body-2">
          Sign in with your email</p>
      </div>
 
      <form name="loginForm" layout="column" layout-fill layout-padding layout-margin>
        <md-input-container>
          <label>
            Email
          </label>
          <input type="text" ng-model="login.credentials.email" aria-label="email" required/>
        </md-input-container>
        <md-input-container>
          <label>
            Password
          </label>
          <input type="password" ng-model="login.credentials.password" aria-label="password" required/>
        </md-input-container>
        <div layout="row" layout-align="space-between center">
          <a class="md-button" href="/password">Forgot password?</a>
          
          <md-button class="md-raised md-primary" ng-click="login.login()" aria-label="login" ng-disabled="login.loginForm.$invalid()">Sign In
          </md-button>
        </div>
      </form>
 
      <md-toolbar ng-show="login.error" class="md-warn" layout="row" layout-fill layout-padding layout-margin>
        <p class="md-body-1">{{ login.error }}</p>
      </md-toolbar>
 
      <md-divider></md-divider>
 
      <div layout="row" layout-align="center">
        <a class="md-button" href="/register">Need an account?</a>
      </div>
    </div>
  </md-whiteframe>
</md-content>

Now we can create a view for a mobile version:

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
<md-content layout="row" layout-align="center start" layout-fill layout-margin>
  <md-whiteframe layout="column" flex flex-md="50" flex-lg="50" flex-gt-lg="33" class="md-whiteframe-z2" layout-fill>
    <!-- header -->
    <md-toolbar class="md-primary md-tall" layout="column" layout-align="end" layout-fill>
      <div layout="row" class="md-toolbar-tools md-toolbar-tools-bottom">
        <h3 class="md-display-1">
          Login
        </h3>
      </div>
    </md-toolbar>
 
    <!-- content -->
    <div layout="column" layout-fill layout-margin layout-padding>
      <!-- display an error -->
      <md-toolbar ng-show="login.error" class="md-warn" layout="row" layout-fill layout-padding layout-margin>
        <p class="md-body-1">{{ login.error }}</p>
      </md-toolbar>
 
      <md-divider></md-divider>
 
      <!-- other actions -->
      <div layout="row" layout-align="center">
        <a class="md-button" href="/password">Forgot password?</a>
      </div>
    </div>
  </md-whiteframe>
</md-content>

As you can see for now there are only three elements:

  • md-toolbar with a heading
  • md-toolbar to display an error
  • and a link to password reset section

Now we have views for both versions. Let's take Login class which is component's controller and move it to web.js:

21.4 Move Login class to web.js imports/ui/components/login/web.js
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
import { Meteor } from 'meteor/meteor';
 
export class Login {
  constructor($scope, $reactive, $state) {
    'ngInject';
 
    this.$state = $state;
 
    $reactive(this).attach($scope);
 
    this.credentials = {
      email: '',
      password: ''
    };
 
    this.error = '';
  }
 
  login() {
    Meteor.loginWithPassword(this.credentials.email, this.credentials.password,
      this.$bindToContext((err) => {
        if (err) {
          this.error = err;
        } else {
          this.$state.go('parties');
        }
      })
    );
  }
}

Let's create an empty class with the same name but for mobile inside mobile.js file:

21.5 Create controller for mobile imports/ui/components/login/mobile.js
1
export class Login {}

We have two modules: web and mobile. Now let's do few things with login.js:

  • Remove an old login.html view.
  • Remove an old Login class.
  • Import Login classes from mobile and web modules.
  • Import both recently created views.
  • Create controller and template variables so they can be used in component definition.
  • Use Meteor.isCordova inside conditional statement to set proper value for these variables.

login.js file should look like this:

21.6 Refactor login.js imports/ui/components/login/login.js
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
20
21
22
23
24
25
26
27
 
import { Meteor } from 'meteor/meteor';
 
import webTemplate from './web.html';
import { Login as LoginWeb } from './web';
import mobileTemplate from './mobile.html';
import { Login as LoginMobile } from './mobile';
 
const name = 'login';
const template = Meteor.isCordova ? mobileTemplate : webTemplate;
const controller = Meteor.isCordova ? LoginMobile : LoginWeb;
 
// create a module
export default angular.module(name, [
...some lines skipped...
])
  .component(name, {
    template,
    controller,
    controllerAs: name
  })
  .config(config);
 

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 okland:accounts-phone

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

Latest release of accounts-phone won't work with Meteor 1.3. Until this changes you can use mys:accounts-phone which fixes that issue.

This is a two-step process so let's implement the first one:

21.9 Add phone verification method imports/ui/components/login/mobile.js
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
import { Accounts } from 'meteor/accounts-base';
 
export class Login {
  constructor($scope, $reactive) {
    'ngInject';
 
    $reactive(this).attach($scope);
 
    this.isStepTwo = false;
    this.phoneNumber = '';
    this.verificationCode = '';
    this.error = '';
  }
 
  verifyPhone() {
    Accounts.requestPhoneVerification(this.phoneNumber, this.$bindToContext((err) => {
      if (err) {
        // display also reason of Meteor.Error
        this.error = err.reason || err;
      } else {
        this.error = '';
        // move to code verification
        this.isStepTwo = true;
      }
    }));
  }
}

Let me explain what happened:

  • We used isStepTwo variable to check in which step we currently are.
  • verificationCode will be used in the second step.
  • Accounts.requestPhoneVerification is to verify a phone number.
  • this.isStepTwo = true; is to mark that we moved to the second step

Now we can implement it in the mobile.html:

21.10 Add phone verification form imports/ui/components/login/mobile.html
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    <md-toolbar class="md-primary md-tall" layout="column" layout-align="end" layout-fill>
      <div layout="row" class="md-toolbar-tools md-toolbar-tools-bottom">
        <h3 class="md-display-1">
          Login with SMS
        </h3>
      </div>
    </md-toolbar>
 
    <!-- content -->
    <div layout="column" layout-fill layout-margin layout-padding>
      <!-- step 1: phone verification -->
      <form ng-show="!login.isStepTwo" layout="column" layout-fill layout-padding layout-margin>
        <md-input-container>
          <input type="text" ng-model="login.phoneNumber" placeholder="phone"/>
        </md-input-container>
        <md-button class="md-raised md-primary" ng-click="login.verifyPhone()" aria-label="login" ng-disabled="!login.phoneNumber">Send SMS
        </md-button>
      </form>
      
      <!-- display an error -->
      <md-toolbar ng-show="login.error" class="md-warn" layout="row" layout-fill layout-padding layout-margin>
        <p class="md-body-1">{{ login.error }}</p>

Let's move to second step which is the code verification. We have to create a method for this:

21.11 Add code verification method imports/ui/components/login/mobile.js
1
2
3
4
5
6
7
8
9
10
11
 
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { Accounts } from 'meteor/accounts-base';
 
export class Login {
  constructor($scope, $reactive, $state) {
    'ngInject';
 
    this.$state = $state;
 
    $reactive(this).attach($scope);
 
    this.isStepTwo = false;
...some lines skipped...
      }
    }));
  }
 
  verifyCode() {
    Accounts.verifyPhone(this.phoneNumber, this.verificationCode, this.$bindToContext((err) => {
      if (err) {
        this.error = err.reason || err;
      } else {
        // redirect to parties list
        this.$state.go('parties');
      }
    }));
  }
}
  • We used $state service to redirect user to parties after successful verification.
  • Accounts.verifyPhone is to verify a code with a phone number.

Add it to the mobile.html:

21.12 Add code verification form imports/ui/components/login/mobile.html
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
        <md-button class="md-raised md-primary" ng-click="login.verifyPhone()" aria-label="login" ng-disabled="!login.phoneNumber">Send SMS
        </md-button>
      </form>
      <!-- step 2: code verification -->
      <form ng-show="login.isStepTwo" layout="column" layout-fill layout-padding layout-margin>
        <md-input-container>
          <input type="text" ng-model="login.verificationCode" placeholder="code"/>
        </md-input-container>
        <md-button class="md-raised md-primary" ng-click="login.verifyCode()" aria-label="login" ng-disabled="!login.verificationCode">Verify Code
        </md-button>
      </form>
 
      <!-- display an error -->
      <md-toolbar ng-show="login.error" class="md-warn" layout="row" layout-fill layout-padding layout-margin>
        <p class="md-body-1">{{ login.error }}</p>

And that's it! We've got a the same component that behaves differently for each platform!

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.