The Official Ionic Blog

Build amazing native and progressive web apps with HTML5

ionic-2-twitter-devdactic

Simon Reimler is a Software Developer helping mobile developers through online courses, books and consulting. Simon writes about Ionic frequently on his blog Devdactic. He also just released a book called Ionic 2: From Zero to App Store.

It’s been some time since the first version of this post (actually it was February 2015), but today we can finally move to the next level with Ionic 2.

In this tutorial, we will develop a simple Twitter app with Ionic 2, in which a user can login with his stored Twitter account and read out his own timeline. Additionally, we will be able to send out tweets directly from our app.

We will use the Twitter Connect plugin from Ionic native, which uses Fabric under the hood. You can sign up for free to use the tool, it’s even a Twitter company, so this isn’t a random external tool you need to integrate.

Before we start, here’s a little gif that shows our end result.

ionic-2-twitter-app-animated-devdactic

Let’s get the party started!

Prerequisites

As I mentioned, we’ll need some API keys. First, you need a Twitter App because otherwise you won’t get any token and won’t be alowed to make specific calls like we want to. Go ahead and create an app; there’s nothing special you have to insert into those fields.

Make sure to have your Access Level set to at least Read and Write, but if you also add the permission for “direct messages” you’ll get the most power through the API later.

The next step is to get a key from Fabric. As the Twitter connect plugin states, there are 3 steps:

  1. Login to your Fabric account (or create one, if you don’t have one) and go to the Twitter install guide.
  2. Find the AndroidManifest.xml code block on that page.
  3. Find the API Key inside that code block.

The block should look like this:

AndroidManifest.xml
      <meta-data
          android:name="io.fabric.ApiKey"
          android:value="FABRIC_KEY"
      />

It’s not very hard to find these keys, so now you should have both your Twitter App keys and Fabric keys ready and at hand. You are ready to add the Twitter connect plugin to your app!

Starting our little Ionic 2 Twitter client

As always in my tutorials on my blog, we will create a blank new app and add everything we need as we go. We’ll start with the blank template and install the Twitter connect plugin from Ionic native and pass our Fabric Key in the install statement.

Additionally, we need the JSSHA library at the version 1.6 to sign our calls to the Twitter API in the right way. Finally, we’ll generate a login page and a provider for all of our calls, so go ahead and run:

Ionic start devdactic-twitter blank --v2
Cd devdactic-twitter
ionic plugin add cordova-plugin-inappbrowser
ionic plugin add twitter-connect-plugin --variable FABRIC_KEY=fabric_API_key
npm install [email protected] --save
npm install @types/jssha --save-dev
ionic g page login
ionic g provider twitterUtils

Now, you need the consumer key and secret key from your Twitter app, because they need to be added to your config.xml. You can find the values inside your Twitter app inside Keys and Access Tokens -> Application Settings. Go ahead and add these two entries inside the config.xml:

<preference name="TwitterConsumerKey" value="<Twitter Consumer Key>" />
<preference name="TwitterConsumerSecret" value="<Twitter Consumer Secret>" />

We have added some pages and providers, so it’s time to put all of them inside the src/app/app.module.ts so they get connected correctly. Insert everything below inside that file:

import { NgModule, ErrorHandler } from [email protected]/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { TwitterUtils } from '../providers/twitter-utils';
import { LoginPage } from '../pages/login/login';


@NgModule({
  declarations: [
    MyApp,
    HomePage,
    LoginPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    LoginPage
  ],
  providers: [TwitterUtils, {provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

The pages go into declarations and entryComponents and the provider inside the providers array, as always!

As we have already added a login page, let’s make that page the starting point of our app. Therefore, open the src/app/app.components.ts and insert:

import { Component } from [email protected]/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { LoginPage } from '../pages/login/login';


@Component({
  template: ``
})
export class MyApp {
  rootPage = LoginPage;


  constructor(platform: Platform) {
    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.
      StatusBar.styleDefault();
      Splashscreen.hide();
    });
  }
}

The app will now start with the LoginPage, and we have everything we need for our Ionic 2 Twitter app in place, so let’s start coding our provider!

Creating a Twitter Provider

As all of the Twitter API calls need some special treatment, the class for those calls won’t be that beautiful. But if you simply put everything inside a provider, you can use all the calls without any problems inside the rest of the app and won’t see that stuff anymore.

It would be a good idea to wrap this into a NPM package, but I had no luck creating a package with the needed dependencies. If someone is interested in doing this or knows how to, please leave a comment below!

The next code block for the provider will be pretty long, because it’s kinda tricky to sign your request to the Twitter REST API correctly. Anyway, you only have to worry about the public functions of that provider, as those are the only ones you will use.

These are:

  • configureUtils(cK, cS, oK, oS): You call this function once you got an OAuth token from the Twitter Connect login and pass all the parameters that our provider needs one time.
  • performGetRequest(url, neededParams, optionalParams?): Perform all GET requests with this single function.
  • performPostRequest(url, neededParams, optionalParams?): Like before, but use this function for all POST requests.

The rest of the provider is used to create the correct signature, transform parameters, and more stuff that Twitter expects. I’m not going into the details of each function here, as it would blow up this tutorial, and it’s not really interesting for what we want to achieve at this moment. Go ahead and put everything below inside your src/providers/twitter-utils.ts:

import { Injectable } from [email protected]/core';
import { Http, Headers, URLSearchParams, RequestOptions } from [email protected]/http';
import 'rxjs/add/operator/map';
import * as jsSHA from "jssha";


@Injectable()
export class TwitterUtils {
  consumerKey: string;
  consumerSecret: string;
  oauthKey: string;
  oauthSecret: string


  constructor(public http: Http) {}


  public configureUtils(cK, cS, oK, oS) {
    this.consumerKey = cK;
    this.consumerSecret = cS;
    this.oauthKey = oK;
    this.oauthSecret = oS;
  }


  public performGetRequest(url, neededParams, optionalParams?) {
    if (typeof(optionalParams)==='undefined') optionalParams = {};
    if (typeof(neededParams)==='undefined') neededParams = {};
    let parameters = Object.assign(optionalParams, neededParams);


    let signature = this.createTwitterSignature('GET', url, parameters, this.consumerKey, this.consumerSecret, this.oauthKey, this.oauthSecret);


    let headers = new Headers({ 'Accept': 'application/json' });
    headers.append('Authorization', signature['authorization_header']);


    let params = new URLSearchParams();
    for (var key in parameters) {
      params.set(key, parameters[key]);
    }


    return this.http.get(url, {search: params, headers: headers})
    .map(response => response.json());
  }


  public performPostRequest(url, neededParams, optionalParams?) {
    if (typeof(optionalParams)==='undefined') optionalParams = {};
    if (typeof(neededParams)==='undefined') neededParams = {};
    let parameters = Object.assign(optionalParams, neededParams);


    let signature = this.createTwitterSignature('POST', url, parameters, this.consumerKey, this.consumerSecret, this.oauthKey, this.oauthSecret);
    if (parameters !== {}) url = url + '?' + this.transformRequest(parameters);


    let headers = new Headers({ 'Accept': 'application/json' });
    headers.append('Authorization', signature['authorization_header']);


    let options = new RequestOptions({ headers: headers });


    return this.http.post(url, parameters, options)
    .map(response => response.json());
  }


  private createSignature(method, endPoint, headerParameters, bodyParameters, secretKey, tokenSecret) : {} {
   if(typeof jsSHA !== "undefined") {
     var headerAndBodyParameters = Object.assign({}, headerParameters)
     var bodyParameterKeys = Object.keys(bodyParameters);
     for(var i = 0; i < bodyParameterKeys.length; i++) {
       headerAndBodyParameters[bodyParameterKeys[i]] = this.escapeSpecialCharacters(bodyParameters[bodyParameterKeys[i]]);
     }
     var signatureBaseString = method + "&" + encodeURIComponent(endPoint) + "&";
     var headerAndBodyParameterKeys = (Object.keys(headerAndBodyParameters)).sort();
     for(i = 0; i < headerAndBodyParameterKeys.length; i++) {
       if(i == headerAndBodyParameterKeys.length - 1) {
         signatureBaseString += encodeURIComponent(headerAndBodyParameterKeys[i] + "=" + headerAndBodyParameters[headerAndBodyParameterKeys[i]]);
       } else {
         signatureBaseString += encodeURIComponent(headerAndBodyParameterKeys[i] + "=" + headerAndBodyParameters[headerAndBodyParameterKeys[i]] + "&");
       }
     }
     var oauthSignatureObject = new jsSHA(signatureBaseString, "TEXT");


     var encodedTokenSecret = '';
     if (tokenSecret) {
       encodedTokenSecret = encodeURIComponent(tokenSecret);
     }


     headerParameters.oauth_signature = encodeURIComponent(oauthSignatureObject.getHMAC(encodeURIComponent(secretKey) + "&" + encodedTokenSecret, "TEXT", "SHA-1", "B64"));
     var headerParameterKeys = Object.keys(headerParameters);
     var authorizationHeader = 'OAuth ';
     for(i = 0; i < headerParameterKeys.length; i++) {
       if(i == headerParameterKeys.length - 1) {
         authorizationHeader += headerParameterKeys[i] + '="' + headerParameters[headerParameterKeys[i]] + '"';
       } else {
         authorizationHeader += headerParameterKeys[i] + '="' + headerParameters[headerParameterKeys[i]] + '",';
       }
     }
     return { signature_base_string: signatureBaseString, authorization_header: authorizationHeader, signature: headerParameters.oauth_signature };
   } else {
     return {fail: "Missing jsSHA JavaScript library"};
   }
 }


 private createNonce(length) {
   var text = "";
   var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
   for(var i = 0; i < length; i++) {
     text += possible.charAt(Math.floor(Math.random() * possible.length));
   }
   return text;
 }


 private escapeSpecialCharacters(string) {
   var tmp = encodeURIComponent(string);
   tmp = tmp.replace(/\!/g, "%21");
   tmp = tmp.replace(/\'/g, "%27");
   tmp = tmp.replace(/\(/g, "%28");
   tmp = tmp.replace(/\)/g, "%29");
   tmp = tmp.replace(/\*/g, "%2A");
   return tmp;
 }


private transformRequest(obj) {
      var str = [];
      for(var p in obj)
      str.push(encodeURIComponent(p) + "=" + this.escapeSpecialCharacters(obj[p]));
      console.log(str.join('&'));
      return str.join('&');
  }


  private createTwitterSignature(method, url, bodyParameters, clientId, clientSecret, oauthKey, oauthSecret) {
     var oauthObject = {
       oauth_consumer_key: clientId,
       oauth_nonce: this.createNonce(10),
       oauth_signature_method: "HMAC-SHA1",
       oauth_token: oauthKey,
       oauth_timestamp: Math.round((new Date()).getTime() / 1000.0),
       oauth_version: "1.0"
     };
     var signatureObj = this.createSignature(method, url, oauthObject, bodyParameters, clientSecret, oauthSecret);
    return signatureObj;
   }
}

Again, I already tried to bring it into a package, which would be a lot cleaner and easier to use, but this task remains open for the reader!

Of course we now need to actually use that provider, so let’s continue with the rest of the app and use that massive provider.

Building a Twitter Login with Ionic Native

Our first screen won’t get any award for its beautiful design, but this tutorial is more about the function than the UI. Actually, the view will only consists of one button to start the login process.

Therefore, open your src/pages/login/login.html and insert:

<ion-header>
  <ion-navbar color="primary">
    <ion-title>
      Devdactic + Twitter
    </ion-title>
  </ion-navbar>
</ion-header>


<ion-content padding>
  <button ion-button full (click)="loginWithTwitter()">Login with Twitter</button>
</ion-content>

As I said before, nothing special. Let’s continue with the class of that page, which will start the login using Twitter connect.

It’s pretty easy to include the TwitterConnect plugin and use it, so make sure to load everything correctly and inside our loginWithTwitter() function we now only need to call TwitterConnect.login() which will handle the rest.

This plugin will use the Twitter account of the current device, so there won’t be another login form inside a webview like normaly. While testing this plugin I got all the rights for making API requests, I’m not 100% sure how it works internally to give all the permissions out without asking the user again, like it is normaly required for Twitter apps.

If you have deeper insight, please leave a comment!

Anyway, if we get a successful response from the plugin, we can configure our provider. Pass the keys of the Twitter application you created before and now also the tokens of our successful response. Once this is done, we set our root navigation to the HomePage, and we are done with the login.

Put all of the code below inside your src/pages/login/login.ts:

import { Component } from [email protected]/core';
import { NavController, AlertController, LoadingController, Loading } from 'ionic-angular';
import { TwitterConnect } from 'ionic-native';
import { TwitterUtils } from '../../providers/twitter-utils';
import { HomePage } from '../home/home';


@Component({
  selector: 'page-login',
  templateUrl: 'login.html'
})
export class LoginPage {
  loading: Loading;


  constructor(public navCtrl: NavController, public twitterUtils: TwitterUtils, private alertCtrl: AlertController, private loadingCtrl: LoadingController) {}


  public loginWithTwitter() {
    this.showLoading();
    TwitterConnect.login().then((data) => {
      this.onSuccess(data);
    }, error => {
      this.onError(error);
    });
  }


  public onSuccess(response) {
    console.log("success:", response);
    setTimeout(() => {
        this.loading.dismiss();
        this.navCtrl.setRoot(HomePage);
      });
    this.twitterUtils.configureUtils('yourConsumerKey', 'yourConsumerSecret', response.token, response.secret);
  }


  public onError(response) {
    this.showError(response);
  }


  private showLoading() {
    this.loading = this.loadingCtrl.create({
      content: 'Please wait...'
    });
    this.loading.present();
  }


  private showError(text) {
    setTimeout(() => {
      this.loading.dismiss();
    });
    let alert = this.alertCtrl.create({
      title: 'Fail',
      message: text + '\nMake sure to setup Twitter account on your device.',
      buttons: ['OK']
    });
    alert.present(prompt);
  }
}

This is all we need for logging in with Twitter! You can give it a try and see if your login works. The next part now is to load actual data from the REST API!

Getting and sending Tweets from our App

We have all the rights we need, and we have all tokens and providers configured, so now the actual fun begins: Getting data from the Twitter API. I like this part because it’s such a good feeling to finally get real data, after all the earlier steps!

Inside our class, we will have a few interesting functions, so let’s go through all of them.

Once our view enters, we will use the loadTimeline() function to load the timeline of the current user. This function will call the appropriate API endpoint using our own provider and set the tweets array of our class.

The composeTweet() will bring up a simple alert box where we can insert new text for a tweet. Once we finish that box, we call the postTweet() function to actually send out the tweet.

Just like before, the postTweet() will now simply use the correct endpoint and use our provider to post a new status update of the user! Everything is wrapped in some loading animation, but nothing really fancy here.

Those are the main functions of our class, now open the src/pages/home/home.ts and insert:

import { Component } from [email protected]/core';
import { NavController, AlertController, LoadingController, Loading, ToastController } from 'ionic-angular';
import { TwitterUtils } from '../../providers/twitter-utils';
import { InAppBrowser } from 'ionic-native';


@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  loading: Loading;
  tweets = [];
  constructor(public navCtrl: NavController, public twitterUtils: TwitterUtils, private alertCtrl: AlertController, private loadingCtrl: LoadingController, private toastCtrl: ToastController) {}


  public ionViewWillEnter() {
    this.loadTimeline();
  }


  public loadTimeline(refresher?) {
    this.showLoading();
    let url = 'https://api.twitter.com/1.1/statuses/home_timeline.json';
    let params = {count: 10};


    this.twitterUtils.performGetRequest(url, params).subscribe((data) => {
      this.tweets = data;
      this.loading.dismiss();
      refresher.complete();
    }, error => {
      refresher.complete();
      this.showError(error);
    });
  }


  public composeTweet() {
    let prompt = this.alertCtrl.create({
      title: 'New Tweet',
      message: "Write your Tweet message below",
      inputs: [
        {
          name: 'text'
        },
      ],
      buttons: [
        {
          text: 'Cancel'
        },
        {
          text: 'Tweet',
          handler: data => {
            console.log('Saved clicked: ', data.text);
            this.postTweet(data.text);
          }
        }
      ]
    });
    prompt.present();
  }


  public dateForTweet(dateString) {
    console.log("my string: ", dateString);
    let d = new Date(Date.parse(dateString));


    // http://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date
    var datestring = ("0" + d.getDate()).slice(-2) + "-" + ("0"+(d.getMonth()+1)).slice(-2) + "-" +
    d.getFullYear() + " " + ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2);


    return datestring;
  }


  public openLinkUrl(url) {
    let browser = new InAppBrowser(url, 'blank');
    browser.show();
  }


  public postTweet(text) {
    this.showLoading();
    let urlPost = 'https://api.twitter.com/1.1/statuses/update.json';
    this.twitterUtils.performPostRequest(urlPost, {status: text}).subscribe((data) => {
      this.loading.dismiss();
      let toast = this.toastCtrl.create({
        message: 'Tweet posted!',
        duration: 3000
      });
      toast.present();
    }, error => {
      this.showError(error);
    })
  }


  private showLoading() {
    this.loading = this.loadingCtrl.create({
      content: 'Please wait...'
    });
    this.loading.present();
  }


  private showError(text) {
    this.loading.dismiss();
    let alert = this.alertCtrl.create({
      title: 'Error',
      message: text,
      buttons: ['OK']
    });
    alert.present(prompt);
  }
}

The rest of the code consists of some helper functions. With dateForTweet(), we need to calculate a date that we can display for every tweet as the default response from Twitter won’t look very good to a user.

Also, if there is a link attached to a tweet (we get this information from the API) we have a function to open that URL inside the the InAppBrowser (that’s also the reason we installed the Cordova plugin in the beginning!).

The final step now is to hook up everything inside our view. In general, we only need to iterate over the items of our tweets array and display a cool Ionic card for each of them. Additional, we have a button inside our nav-bar to start our tweet composer and also an ion-refresher inside the content to reload the timeline of the user.

For the card of a tweet, we pull out some information from the API; you can find a detailed description of the responses inside the Twitter REST API documentation.
Interesting fields for us are here:

  • Tweet.user.profile_image_url: A link to the profile image of the user
  • Tweet.user.name: Username
  • Tweet.created_at: A timestamp of the creation
  • Tweet.extended_entities.media[0].media_url: If there is media attached, tis would be a link to an attached image (we only take the first one)
  • Tweet.text: The main message of the tweet
  • Tweet.entities.urls: Any URL that is inside the tweet
  • Tweet.entities.urls[0].url: Pick one URL we can open from a tweet

Now, open the src/pages/home/home.html and insert:

<ion-header>
  <ion-navbar color="primary">
    <ion-title>
      My Feed
    </ion-title>
    <ion-buttons end>
      <button ion-button icon-only (click)="composeTweet()">
        <ion-icon name="create"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>


<ion-content padding>
  <ion-refresher (ionRefresh)="loadTimeline($event)">
    <ion-refresher-content></ion-refresher-content>
  </ion-refresher>


  <ion-card *ngFor="let tweet of tweets">


    <ion-item>
      <ion-avatar item-left>
        <img src="{{tweet.user.profile_image_url}}">
      </ion-avatar>
      <h2>{{tweet.user.name}}</h2>
      <p>{{dateForTweet(tweet.created_at)}}</p>
    </ion-item>


    <img src="{{tweet.extended_entities.media[0].media_url}}" *ngIf="tweet.extended_entities">


    <ion-card-content>
      <p>{{tweet.text}}</p>
    </ion-card-content>


    <ion-row>
      <ion-col *ngIf="tweet.entities.urls.length > 0">
        <button ion-button clear small (click)="openLinkUrl(tweet.entities.urls[0].url)">
          <ion-icon name="open"></ion-icon>
          <div>Open Link</div>
        </button>
      </ion-col>
    </ion-row>


  </ion-card>


</ion-content>

You are done! It wasn’t that hard, right?

The Twitter Connect plugin is an easy way to use the device’s Twitter account of a user to pull data from the Twitter REST API. Make sure to run this code on the simulator/device where you have setup a Twitter account inside your system settings.

The app will then look like below!

ionic-2-twitter-devdactic

Conclusion

In this tutorial, you saw how to build a simple Ionic 2 app that uses the Twitter API. If you want to help the community, it might be a good idea for someone to wrap the Twitter provider inside an NPM package, so everyone can easily use it.
Also, you could put the URLs of the REST API directly into the package, so you don’t even have to worry about the URLs anymore inside your own code.

If you want to learn how to build Ionic 2 Apps from Zero to App Store, make sure to check out my just released eBook!

Happy Coding,
Simon

  • faraazc

    You have used ngFor..won’t this be less performance .since it is a data driven app how about going to virtual scroll plus infinite scroll..

  • Max

    Great post. I get the following error, some hints?

    Typescript Error
    Supplied parameters do not match any signature of call target.

    src/providers/twitter-utils.ts

    headerParameters.oauth_signature = encodeURIComponent(oauthSignatureObject.getHMAC(encodeURIComponent(secretKey) + “&” + encodedTokenSecret, “TEXT”, “SHA-1”, “B64”));
    var headerParameterKeys = Object.keys(headerParameters);

    • http://e10.in/ Anuj Pandey

      Any idea ?

      • Max

        Nope! You?

        • uzair

          Facing the same error 🙁

    • Aamir

      same here 🙁

    • Max

      It is probably related to @types/jssha ( https://github.com/Caligatio/jsSHA/issues/51 )

      Getting rid of it I get a


      Error @Component templateUrl missing in:
      ".../twitter-app-ionic-2/src/app/app.component.ts"

      because of the


      @Component({
      template: ``
      })

      Finally:


      @Component({
      templateUrl: 'app.html'
      })

      works.

    • Philson Nah

      Same error here. Can’t build. Anyone has a solution?

    • Yoshinori Satoh

      I found that after second parameters of getHMAC are invalid.
      Second parameter is array.

      /**
      * Returns the the HMAC in the specified format using the key given by
      * a previous setHMACKey call.
      *
      * @param {string} format The desired output formatting
      * (B64, HEX, or BYTES)
      * @param {{outputUpper : (boolean|undefined), b64Pad : (string|undefined)}=}
      * outputFormatOpts associative array of output formatting options
      * @return {string} The string representation of the hash in the format
      * specified
      */
      getHMAC(format:string, outputFormatOpts?:OutputFormatOptions):string

      I have modified as below and work in my local environment.

      headerParameters.oauth_signature = encodeURIComponent(oauthSignatureObject.getHMAC(encodeURIComponent(secretKey) + “&” + encodedTokenSecret, [“TEXT”, “SHA-1”, “B64”]));

      • Francesco Mussi

        This fixed the problem for me! Thanks!

  • https://github.com/stefanhuber Stefan Huber

    How do you do the unit or functional testing? Twitter uses that for their app, so the post is misleading! A twitter app without testing is probably malware…

  • Vincent Bergeron

    I think he uses old code for newer jsSHA library… I get the same error