Sunday, June 6, 2010

How to integrate Facebook's JavaScript SDK with GWT

First of all - you need to have a Facebook account. Then you need to create an application on Facebook at http://www.facebook.com/developers/createapp.php, that will give you an App Id. This id is tightly connected to the url/site from which you will be using the JavaScript SDK and you'll be using it later.

For development, localhost addresses works fine - i.e. http://127.0.0.1:8888.

Next thing is to add the actual JavaScript library to you hostpage, which according to Facebook means adding this piece of JavaScript just before the body-endtag:

<!-- Facebook integration -->
<div id="fb-root"></div>
<script>
(function() {
var e = document.createElement('script'); e.async = true;
e.src = document.location.protocol +
'//connect.facebook.net/en_US/all.js';
document.getElementById('fb-root').appendChild(e);
}());
</script>

It will dynamically add a script-tag into your html-file, which bypasses the "Same Origin Policy" all together. (No need for a xd_receiver.htm, as with previous versions of the API, that is).

Now it's time to initialize the Facebook API using native methods in GWT:

private native String initFacebookAPI()
/*-{
$wnd.FB.init({appId: '<YOUR_APP_ID_HERE>', status: true, cookie: true, xfbml: true});
$wnd.FB.Event.subscribe('auth.sessionChange', function(response) {
if (response.session) {
// A user has logged in, and a new cookie has been saved
$wnd.onLogin();
} else {
// The user has logged out, and the cookie has been cleared
$wnd.onLogout();
}          
});
}-*/;

$wnd.FB works because we dynamically included the Facebook .js-file in the previous step. So, anything you'd want to do when initializing the API can be done here, e.g. adding subscriptions to events (as above).

As can be seen, two different methods are being called when a sessionChange-event occurs (depending on what happened). These methods will be regular GWT-methods, but we need to export them into the DOM in order to call them from within native code:

private native void exportMethods(Application instance) /*-{
$wnd.onLogin = function() {
return instance.@com.technowobble.gwt.client.Application::onLogin()();
}
$wnd.onLogout = function() {
return instance.@com.technowobble.gwt.client.Application::onLogout()();
}
$wnd.onAPICall = function(callback, response, exception) {
return instance.@com.technowobble.gwt.client.Application::onAPICall

(Lcom/google/gwt/user/client/rpc/AsyncCallback;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)

(callback, response, exception);
}
}-*/;

There's also on more function being exported (which needs some explanation...) It serves as a callback function for a native method exposing the Facebook Graph API:

private native void callAPI(String path, AsyncCallback<JavaScriptObject> callback) /*-{
$wnd.FB.api(path, function(response) {
if (!response) {
alert('Error occured');
} else if (response.error) {
alert($wnd.dump(response));
// call callback with the actual error
$wnd.onAPICall(callback, null, response.error);
} else if (response.data) {
alert($wnd.dump(response));
// call callback with the actual json-array
$wnd.onAPICall(callback, response.data, null);
} else {
alert($wnd.dump(response));
// call callback with the actual json-object
$wnd.onAPICall(callback, response, null);
} 
});
}-*/;

What happens here is that any GWT-method now can make calls like this:

callAPI("/me", new AsyncCallback<JavaScriptObject>() {

@Override
public void onFailure(Throwable caught) {
Window.alert(caught.getMessage());
}

@Override
public void onSuccess(JavaScriptObject result) {
UserJso fbUser = (UserJso) result;
Window.alert(fbUser.getFullName());
}
});   

The onAPICall exported previously is the method binding the first call to the AsyncCallback:

public void onAPICall(AsyncCallback<JavaScriptObject> callback,
JavaScriptObject response, JavaScriptObject exception) {
if (response != null) {
callback.onSuccess(response);
} else {
ExceptionJso e = (ExceptionJso) exception;
callback.onFailure(new Exception(e.getType() + ": " + e.getMessage()));
}
}

Note that the AsyncCallback used for a specific Graph API call, e.g. "/me" is free to cast the result to an overlay type (the result should be a json-representation). The UserJso looks like this:

package com.technowobble.gwt.client.json;

import com.google.gwt.core.client.JavaScriptObject;

public class UserJso extends JavaScriptObject {
// Overlay types always have protected, zero-arg constructors
protected UserJso() { }

public final native String getFirstName() /*-{ return this.first_name; }-*/;
public final native String getLastName()  /*-{ return this.last_name;  }-*/;

public final String getFullName() {
return getFirstName() + " " + getLastName();
}
}

A full example project can be found here and should get you started on more elaborative usage.

9 comments:

  1. Great use of GWT =)
    Only challenge I had with your example was the lack of a "wait until $wnd.FB !== undefined" before any FB calls.
    Thanks for sharing!

    ReplyDelete
  2. Thank you very much, Mattias.
    This article is very helpful for me.

    I am using Smartgwt. I modified a little then the codes can work.

    Still the same problem as George said, "undefined".

    I tried to use " if (typeof($wnd.FB)== undefined) " or "try{}catch(e){}" before "$wnd.FB.api" call, but it doesn't work.

    Can you give me a little tips how to do this?
    I am a newbie of Javascript.

    ReplyDelete
  3. Hi,

    I've actually never had any problems with the FB-object not being ready when I've accessed it...

    I guess one viable option would be add the FB-api in a non-async way, or listen to some callback function that would update some "FBReady" variable that you could check before using the object?

    Let's ask other readers to collaborate to come up with a working example, shall we?

    ReplyDelete
  4. I also have the problem with $wnd.FB == undefined. It is incrediibly annoying because sometimes it works and sometimes (especially when I need it) it does not. I am completely stuck here. Did anyone come up with a solution yet?
    Thanks!!

    Malte

    ReplyDelete
  5. Excellent Post, it was very helpful and simple.
    Regarding the problem of the time it takes for the FB object to be loaded, you can use the suggestion for Asynchronous Loading from FB site: http://developers.facebook.com/docs/reference/javascript/fb.init/

    In general I moved the code for initialization of the FB object from the GWT code to the HTML file


    ...
    window.fbAsyncInit = function() {
    FB.init({appId: 'YOUR-APP-ID', status: true, cookie: true,
    xfbml: true});
    FB.Event.subscribe('auth.sessionChange', function(response) {
    if (response.session) {
    // A user has logged in, and a new cookie has been saved
    onLogin();
    } else {
    // The user has logged out, and the cookie has been cleared
    onLogout();
    }         
    });
    };
    (function() {
    var e = document.createElement('script');
    e.async = true;
    e.src = document.location.protocol
    + '//connect.facebook.net/en_US/all.js';
    document.getElementById('fb-root').appendChild(e);
    }());
    ...

    ReplyDelete
  6. Great post. I was in the process of figuring this out and it saved me lots of time.

    I believe I fixed the "undefined" problem in my code by exporting my GWT based init code to "$wnd.fbAsyncinit"...the callback FB will look for. That way the FB code calls your init code itself once its loaded instead of you trying to time it. Then all you have to do is call your "export" method early on so its there before FB is loaded. Its basically the same as what Guy has posted above but lets you still write Java if you wish.

    ReplyDelete
  7. I agree with Phil that doing it in GWT code is exactly the same if you prefer it over plain javascript.

    Another issue that I faced was the case that the user is already logged in into facebook and the page/application. I didn't receive the 'auth.sessionChange' event to trigger the onLogin method.

    Therefore, I added a call to getLoginStatus right after the init, with the same onLogin and onLogout calls:


    FB.getLoginStatus(function(response) {
    if (response.session) {
    // A user has logged in, and a new cookie has been saved
    onLogin();
    } else {
    // The user has logged out, and the cookie has been cleared
    onLogout();
    }         
    });

    ReplyDelete
  8. check this out, properly working:
    http://code.google.com/p/gwt-gae-fb/

    ReplyDelete