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"/> 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" /> 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.