Tuesday, November 30, 2010

Phonegap, JQuery Mobile, Twitter and Facebook

Recently I have been working on a mobile website that I wanted to turn into a iOS app. Not knowing (or wanting to know) Objective C, I decided to use PhoneGap to build a HTML/Javascript/CSS based application. My mobile website uses JQuery Mobile, so I wanted to continue to use that framework if at all possible (it offers a lot of easy mobile styling and touch based events). My application also makes substantial use of the Facebook and Twitter APIs, so I also needed to find a way to authenticate with those services (both use a form of OAuth).

The first thing I tackled was the OAuth integration portions. I naively thought that the two services respective Javascript login procedures would work in the WebKit window that PhoneGap creates, however I found neither operated. That meant that I had to fall back on doing actual OAuth. I found a great blog post describing how to do this for Facebook, however it didn't do everything I needed it to do, so I thought I describe what I did in more detail.

The first thing you need to do is download the ChildBrowser Plugin and drag and drop the files under the ChildBrowser/iPhone to the plugins folder of your XCode project. Then put the ChildBrowser.js file in your www folder and make sure to include the file as a script tag in your html files AFTER the phonegap.js file.
<script src="phonegap.js" type="application/x-javascript" charset="utf-8"></script>
<script src="ChildBrowser.js" type="application/x-javascript" charset="utf-8"></script>
The next step is to fire off the following when you want to login to Facebook (like a click event or the like).
function(){
var my_client_id = "YOUR_CLIENT_ID", my_redirect_uri = "http://www.facebook.com/connect/login_success.html", my_type = "user_agent", my_display = "touch";

var authorize_url = "https://graph.facebook.com/oauth/authorize?";
authorize_url += "client_id=" + my_client_id;
authorize_url += "&redirect_uri=" + my_redirect_uri;
authorize_url += "&display=" + my_display;
authorize_url += "&scope=read_stream,publish_stream,offline_access,publish_checkins"

client_browser = ChildBrowser.install();
client_browser.onLocationChange = function(loc){
facebookLocChanged(loc);
};
if (client_browser != null) {
window.plugins.childBrowser.showWebPage(authorize_url);
}
});
This basically does the first step of the OAuth flow and shows the Facebook Authorization page int a ChildBrowser window.

The next thing we do is wait for a change in the location of the ChildBrowser window to the redirect_uri.
function facebookLocChanged(loc){
/* Here we check if the url is the login success */

if (loc.indexOf("http://www.facebook.com/connect/login_success.html") > -1) {
client_browser.close();
var fbCode = loc.match(/code=(.*)$/)[1]
$.ajax(
{
url:'https://graph.facebook.com/oauth/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&code='+fbCode+'&redirect_uri=http://www.facebook.com/connect/login_success.html',
data: {},
success: function(data, status){
localStorage.facebook_token = data.split("=")[1];
client_browser.close();
initialize_facebook();
},
error: function(error) {
client_browser.close();
},
dataType: 'text',
type: 'POST'
}
)
}
}
This completes the OAuth process closes the ChildBrowser (I close the ChildBrowser first because I like the user experience) and stores the facebook_token. Note here we have taken advantage of the fact that Cross-Domain AJAX calls are allowed when using PhoneGap. We can now make Facebook calls. I wrote a little helper method for appending on our facebook_token to urls when making calls.
function create_facebook_url(base) {
if(device) {
if(base.indexOf("?") == -1) {
return base + "?access_token=" + localStorage.facebook_token
} else {
return base + "&access_token=" + localStorage.facebook_token
}
} else {
return base;
}
}

// Example of using create_facebook_url
FB.api(create_facebook_url("/me"), function(result){
current_facebook_user = result;
}

For Twitter we follow a similar procedure, though the Twitter OAuth is markedly more complicated. To handle some of the more complex OAuth operations I opted to use an oauth javascript library, so this also needs to be included in your index.html files.
<script src="/javascripts/sha1.js" type="text/javascript"></script>
<script src="/javascripts/oauth.js" type="text/javascript"></script>
Note the sha1.js file is included with the oauth library.

Once we have the libraries included we can now start the OAuth process.
accessor =
{ consumerKey : "YOUR_CONSUMER_KEY"
, consumerSecret: "YOUR_CONSUMER_SECRET"
, serviceProvider:
{ signatureMethod : "HMAC-SHA1"
, requestTokenURL : "http://api.twitter.com/oauth/request_token"
, userAuthorizationURL: "https://api.twitter.com/oauth/authorize"
, accessTokenURL : "https://api.twitter.com/oauth/access_token"
, echoURL : "http://localhost/oauth-provider/echo"
}
};

var message = {
method: "post", action: accessor.serviceProvider.requestTokenURL
, parameters: [["scope", "http://www.google.com/m8/feeds/"]]
};
var requestBody = OAuth.formEncode(message.parameters);
OAuth.completeRequest(message, accessor);
var authorizationHeader = OAuth.getAuthorizationHeader("", message.parameters);
var requestToken = new XMLHttpRequest();
requestToken.onreadystatechange = function receiveRequestToken() {
if (requestToken.readyState == 4) {
var results = OAuth.decodeForm(requestToken.responseText);
var oauth_token = OAuth.getParameter(results, "oauth_token");
var authorize_url = "http://api.twitter.com/oauth/authorize?oauth_token="+oauth_token;
client_browser = ChildBrowser.install();
client_browser.onLocationChange = function(loc){
twitterLocChanged(loc, requestToken, accessor);
};
if (client_browser != null) {
window.plugins.childBrowser.showWebPage(authorize_url);
}
}};
requestToken.open(message.method, message.action, true);
requestToken.setRequestHeader("Authorization", authorizationHeader);
requestToken.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
requestToken.send(requestBody);
Basically we are forming the proper OAuth request to get a request_token from Twitter and once we receive back our token we use it to open a ChildBrowser pointing at the proper Authorization page. Next we are (same as Facebook) look for a location change on our ChildBrowser. This time we look for the location to change to the callback that we set in our Twitter Application definition.
function twitterLocChanged(loc, requestToken, accessor){
/* Here we check if the url is the login success */
if (loc.indexOf("http://www.headingtoo.com/") > -1) {
client_browser.close();
var results = OAuth.decodeForm(requestToken.responseText);
message = {method: "post", action: accessor.serviceProvider.accessTokenURL};
OAuth.completeRequest(message,
{ consumerKey : accessor.consumerKey
, consumerSecret: accessor.consumerSecret
, token : OAuth.getParameter(results, "oauth_token")
, tokenSecret : OAuth.getParameter(results, "oauth_token_secret")
});
var requestAccess = new XMLHttpRequest();
requestAccess.onreadystatechange = function receiveAccessToken() {
if (requestAccess.readyState == 4) {
var params = get_url_vars_from_string(requestAccess.responseText);
localStorage.twitter_token = params["oauth_token"];
localStorage.twitter_secret_token = params["oauth_token_secret"];
localStorage.twitter_user_name = params["screen_name"];
localStorage.twitter_user_id = params["user_id"];
intialize_twitter();
}
};
requestAccess.open(message.method, message.action, true);
requestAccess.setRequestHeader("Authorization", OAuth.getAuthorizationHeader("", message.parameters));
requestAccess.send();
}
}
This code is fired when we notice our ChildBrowser has gone to the defined callback url, we then complete the OAuth process by asking for an access_token, which we put in local storage. We can then use that access token to make request on behalf of our user.
// helper
function get_url_vars_from_string(url) {
var vars = [], hash;
var hashes = url.slice(url.indexOf('?') + 1).split('&');
for(var i = 0; i < hashes.length; i++)
{
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
return vars;

}

// helper
function create_twitter_oauth_ajax_data(url, method, params, success, error) {
var ajax_data = {
url: url,
data: params,
dataType: 'json',
type: method,
timeout: 60*1000,
beforeSend: function(req){
var message = {
method: method,
action: url
};
var message_params = params;
message.parameters = message_params;
debug_log(message.parameters);
OAuth.completeRequest(message, {
consumerKey: accessor.consumerKey,
consumerSecret: accessor.consumerSecret,
token: localStorage.twitter_token,
tokenSecret: localStorage.twitter_secret_token
});
debug_log(message);
req.setRequestHeader("oauth_consumer_key", accessor.consumerKey);
req.setRequestHeader("oauth_nonce", message.parameters['oauth_nonce']);
req.setRequestHeader("oauth_signature_method", 'HMAC-SHA1');
req.setRequestHeader("oauth_token", localStorage.twitter_token);
req.setRequestHeader("oauth_timestamp", message.parameters['oauth_timestamp']);
req.setRequestHeader("oauth_version", '1.0');
req.setRequestHeader("Authorization", OAuth.getAuthorizationHeader("", message.parameters));
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
},
success: success,
error: error
}
return ajax_data;
}
var success = function(data, responseText) {
current_twitter_user = data;
};
var error = function(error){
debug_log(error['responseText']);
current_twitter_user = null;
debug_log("problem getting twitter user");
};
var ajax_data = create_twitter_oauth_ajax_data('http://api.twitter.com/1/users/show.json', 'get', {screen_name:localStorage.twitter_screen_name, user_id:localStorage.twitter_user_id}, success, error);
$.ajax(
ajax_data
);
Above I show some helper methods I created and a call to get a user's data (which doesn't necessarily need authentication, but demonstrates how it is done anyway).

The final thing that I learned has to do with the current version of JQuery Mobile. I was having trouble getting the app to switch pages when the pages were all in different files. JQuery Mobile is supposed to do an AJAX request for the separate files and then include them in the "one-page-app" that is creates. This didn't seem to be working, so what I did was include all my pages in the index.html page, and stopped trying to use separate files. To do this you just have to build separate divs with each of the pages.
<div data-role="page">

<div id="home_page" data-role="header">
</div><!-- /header -->

<div class="home_page" data-role="content">
<div id="main">
<div id="home">
<div id="logins">
<span id="twitter_login" class="button twitter"><img id="twitter_login_button" alt="twitter login" src="/images/twitter_16.png"/>&nbsp;Login</span><span class="logout no_show" id="twitter_logout_container" style="display:none"><img id="twitter_avatar" alt="" height="16" src="" width="16"><img src="/images/twitter_16.png" alt='Twitter' title="Twitter" /><a id="twitter_logged_in_link" href="#" target="blank"></a><a href="#" id="twitter_logout" class="logout">logout</a></span>
<span id="facebook_login" class="button facebook"><img src="/images/facebook_16.png" />&nbsp;Login</span><span class="logout no_show" id="facebook_logout_container" style="display:none;"><img id="facebook_avatar" alt="" height="16" src="" width="16"><img src="/images/facebook_16.png" alt='Facebook' title="Facebook" /><a id="facebook_logged_in_link" href="" target="blank"></a><a id="facebook_logout" href="#" class="logout">logout</a></span>
</div>

<div class="logo">
<img src="/images/logo.png"/>
</div>
<input type="text" id="where" value="Where are you headed?"/>
<div id="where_message_container">
<textarea id="where_message"></textarea>
<a href="#" id="where_message_submit">Submit</a>
</div>
</div>
</div>
<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
FB.init({appId: '161124300591022', status: true, cookie: true,
xfbml: true, scope:'publish_checkins'});
</script>
</div>
<!-- /content -->

<div data-role="footer">
<div class="ui-grid-b">
<div class="ui-block-a">
<a href="#ui-page-start" data-role="button">
Heading Too
</a>
</div>
<div class="ui-block-b">
<a href="#/mobile/friends_headingtoos.html" data-role="button">
Friends
</a>
</div>
<div class="ui-block-c">
<a href="#/mobile/nearby_headingtoos.html" data-role="button">
Everyone
</a>
</div>
</div><!-- /grid-a -->
</div><!-- /page -->
</div>
<div id="/mobile/friends_headingtoos.html" data-role="page">

<div data-role="header">
<h1>Friends</h1>
</div><!-- /header -->

<div data-role="content">
<div id="friends_headingtoos">
<ul></ul>
</div>
</div><!-- /content -->
<div data-role="footer">
<div class="ui-grid-b">
<div class="ui-block-a">
<a href="#ui-page-start" data-role="button">
Heading Too
</a>
</div>
<div class="ui-block-b">
<a href="#/mobile/friends_headingtoos.html" data-role="button">
Friends
</a>
</div>
<div class="ui-block-c">
<a href="#/mobile/nearby_headingtoos.html" data-role="button">
Everyone
</a>
</div>
</div><!-- /grid-a -->
</div><!-- /header -->
</div>
<div id="/mobile/nearby_headingtoos.html" data-role="page">
<div data-role="header">
<h1>Nearby</h1>
</div><!-- /header -->

<div data-role="content">
<div id="nearby_headingtoos">
<div class="map"></div>
<ul></ul>
</div>
</div>
<div data-role="footer">
<div class="ui-grid-b">
<div class="ui-block-a">
<a href="#ui-page-start" data-role="button">
Heading Too
</a>
</div>
<div class="ui-block-b">
<a href="#/mobile/friends_headingtoos.html" data-role="button">
Friends
</a>
</div>
<div class="ui-block-c">
<a href="#/mobile/nearby_headingtoos.html" data-role="button">
Everyone
</a>
</div>

</div><!-- /grid-a -->
</div><!-- /header -->

Basically just adding in DIV's with the proper id's guarantees they will be loaded when a link with a href="#the_id" is clicked. This works with much more regularity than using external files. Since JQuery Mobile is so young I'd guess that this will be a non-issue at some point.

As always ask questions and leave feedback in the comments.

34 comments:

  1. Thank you for this post - this was almost exactly what I was trying to do, and found this really helpful!

    ReplyDelete
  2. Hi: Great post. Would you be interesting in showcasing your work & giving a lecture in the business school at the University of Utah? I look forward to hearing from you.

    Best

    ReplyDelete
  3. Great post. Thank you.

    Do you include the fbconnect.js SDK in your index.html file? How would you now go about posting something to the users FB feed?

    -Marc

    ReplyDelete
  4. Great tutorial... would be possible to post the entire code in just on file

    Thanks

    Rodrigo

    ReplyDelete
  5. Rodrigo, short answer is yes, long answer is I have a 4 month old so I'll have to add it to my list :)

    James

    ReplyDelete
  6. Marc- sorry it took so long to respond. No we don't use the fbconnect.js file, we are simply using the basic facebook REST API directly from javascript. Normally we would need the fbconnect.js b/c we'd need to set up a crossdomain.xml file due to browser security sandboxes, however phonegap gives us the ability to do pure crossdomain ajax calls, so we don't need anything special (javascript-wise) from Facebook.

    James

    ReplyDelete
  7. I managed to get to the point of getting the oauth_token and oauth_token_secret, but then the formatting of the rest of the document is corrupted. If you look at get_url_vars_from_string onwards its all garbled.

    ReplyDelete
  8. I tried to fix the format. Hopefully that helps.

    ReplyDelete
  9. Hey thanks for fixing the formatting, I am still struggling a little. I don't see where you are getting hold of the access_token. Also there is a call to intializeTwitter() but I don't see that method anywhere. Any help is appreciated.

    ReplyDelete
  10. Ok ignore my last comment about access_token, I don't think you need it in Twitter that's for Facebook. I'll carry on and see if I can crack this.

    ReplyDelete
  11. Hi Jamie,

    Good article. I dont get any ajax reply from facebook after updating YOUR_CLIENT_ID and YOUR_CLIENT_SECRET. Neither success nor error code is executing. Any suggestion ?

    $.ajax(
    {
    url:'https://graph.facebook.com/oauth/access_token? client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&code='+fbCode+'&redirect_uri=http://www.facebook.com/connect/login_success.html',
    data: {},
    success: function(data, status){
    localStorage.facebook_token = data.split("=")[1];
    client_browser.close();
    initialize_facebook();
    },
    error: function(error) {
    client_browser.close();
    },
    dataType: 'text',
    type: 'POST'
    }
    )

    Regards
    Abhilash Vijayakumar

    ReplyDelete
  12. Is there any way to display 2 splash screens by giving time duration while loading iPhone app. Because my app is quite heavy and takes 12-15 secs to load. Could you please give some code sample.



    Thanks.

    ReplyDelete
  13. Thank you. It is a very interesting article. I don't know if the coding itself is incomplete and we have to customize things other than ID, KEY or SECRET ... 
    The button in the HTML (jQuery) part is not working for submitting post.
    Any idea? It seems there should be some other js to trigger the login actions. Right? 

    ReplyDelete
  14. Hi, fantastic article. I am trying to do some Twitter integration using an Android Phonegap app but i'm having problems.

    When initialising your child browser for iOS you do the following:

    client_browser = ChildBrowser.install(); client_browser.onLocationChange = function(loc){ twitterLocChanged(loc, requestToken, accessor); };


    This is done differently in android and we don't get the 'client_browser' object  so we can't add the listener 'onLocationChange' to it.

    Any idea's how I can add a listener to the child browser in my case?

    Thanks,
    Dean

    ReplyDelete
  15. Hi, how safe is to keep consumer key and secret in a js variable?

    ReplyDelete
  16. Was able to model your code inside a TwitterConnect.js class, however when testing my phonegap application inside of a rails app, I get the following error when trying to
    make an xhr request:

    XMLHttpRequest cannot load https://api.twitter.com/oauth/request_token. Origin http://localhost:3000 is not allowed by Access-Control-Allow-Origin.

    Any tips?

    ReplyDelete
  17. Hello I am Amarendra Basuri.
    This is posted from DISQUS App.

    ReplyDelete
  18. Sounds to me like you are having browser sandbox problems, my code works in phonegap because in an app the cross-domain restrictions are no longer applicable.

    ReplyDelete
  19. No I didn't we aren't use twitter @anywhere or the facebook JS SDK in this case.  Instead we are directly doing the API calls to the endpoints, doing all the signing off the requests ourselves.

    ReplyDelete
  20. Since this is an app, and therefore is not accessible from Firebug or the like, it is plenty safe.

    ReplyDelete
  21. Hi,
    Thanks for this great post. I was just wondering if Twitter status updates are possible with OAuth. When I try to update user's status with api.twitter.com/1/statuses/update.json it gives the following error: "Could not authenticate you."

    Thanks

    ReplyDelete
  22. We are using your example but keep getting invalid/expired token can u please help

    ReplyDelete
  23. The kind of people who would use the consumer secret are hackers and they all tend to jailbreak their devices (im included) its dead easy to find this secret in a simple ssh session. Please do let me know the name of your iphone app.... :)

    ReplyDelete
  24. Hey! Love the code.

    Dont know at all what happened today with facebook or whatever, but I had to add a decodeURIComponent around the fbCode on this line:

    url:'https://graph.facebook.com/oauth/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&code='+fbCode+'&redirect_uri=http://www.facebook.com/connect/login_success.html',     

    ReplyDelete
  25. If you are talking about cracking iphone/android apps and looking at the source, then yes this is vulnerable.  As are a plethora of other apps out there that integrate with twitter.  Frankly if you want to steal my consumer secret, go ahead, I'll just reset it.... that is kind of the point of oauth.

    ReplyDelete
  26. You'll need to use an auth token when you make that request.  There are examples of how to get that signed token and append it to the request in the code.

    ReplyDelete
  27. Interesting, I don't doubt it is needed, this code is getting ancient in API timeframes...

    ReplyDelete
  28. This has been a huge help getting started on something, but I've had to make some significant changes.

    1) I had to change the way the get_url_vars_from_string() method worked
    2) I had to remove intialize_twitter(); as there was no method actually defined as such
    3) Same for debug_log()

    Not being critical, because this still saved me HOURS or perhaps days of programming, and for that I am eternally thankful. :)

    ReplyDelete
  29. This has been a great jumping-off point for me, but I had to change several things to get it to work:

    1) I had to change the way get_url_vars_from_string() worked (had to comment out a line)
    2) I had to remove intialize_twitter() because it seems that it's not actually defined anywhere
    2) Same for debug_log()

    I'm not being critical, but I thought I'd share that for other people
    coming across this.  I'm extremely grateful either way, as this has
    still saved me hours (perhaps days) of coding!

    ReplyDelete
  30. no need to crack or hack. your app-secret is sitting in cleartext in a js file on the file system. If you reset it, all your current installed devices will have to update the app with a new secret...and the circle repeats. With your secret i can start impersonate your app and submit tons of spam tweets or whatever through your users etc...the harm will have been done long  before you can hit that reset button.  Your users will be pissed at best. This is not a safe approach and should be coupled with a server component to make it secure. There is a reason why a secret should remain a secret.

    ReplyDelete
  31. http://wiki.phonegap.com/w/page/43660891/Security

    Addreses your concerns in the last paragraph. To your valid point, a server component could load the js with the secret dynamically over https to make it secret.

    ReplyDelete
  32. Can you please point to one of those examples? can't find one.

    Tnaks

    ReplyDelete
  33. How do you post after getting the access token? Was it inside the initialize_twitter() method that is missing in the example?

    ReplyDelete
  34. Hey Jamie, thanks for the post. This was absolutely helpfull!

    ReplyDelete