Facebook login using the Facebook SDK through foreign code

Fuse's foreign code feature allows you to integrate native code with Uno code. We currently support Java on Android and Objective-C on iOS. This example shows how to use this feature to integrate the Facebook SDK with Fuse to get Facebook's login functionality.

This example is meant to be a free-standing introduction to foreign code, but it might be useful to consult the technical documentation which for example explains the marshalling between Uno types and native types in more detail. This guide also assumes some fluency in Uno, our C# dialect. See the Uno language reference for more information about Uno.

Android

We'll be following the Facebook login Android guide closely, so if you want to follow along in your own code you should make sure that you have gone through their prerequisites. We'll use the LoginManager class to initiate login by pressing a Fuse button.

Triggering and responding to login

The main login functionality is implemented in an Uno method with the following signature:

public void Login(Action<AccessToken> onSuccess, Action onCancelled, Action<string> onError)

This method is contained in the FacebookLogin class and accepts actions that will be invoked depending on the outcome of the login.

To be able to write the method body in Java we add the [Foreign(Language.Java)] attribute to it and use @{ and @} in place of normal braces. The special braces tell Uno to not parse the code as normal Uno code, and the attribute tells the compiler what language the code is actually written in (Java in this case). We can only use this version of the method on Android, so we use extern(Android) to only compile it when targeting Android.

[Foreign(Language.Java)]
public extern(Android) void Login(Action<AccessToken> onSuccess, Action onCancelled, Action<string> onError)
@{
@}

The AccessToken class, used as the argument to the onSuccess action, is a thin wrapper around a Java object:

public class AccessToken
{
	extern(Android) Java.Object _token;

	public extern(Android) AccessToken(Java.Object token)
	{
		_token = token;
	}
}

The Uno class Java.Object provides a way to hold on to a Java Object on the Uno side, making it possible to store and pass around results obtained from Java code in Uno.

Note: The AccessToken class is not strictly necessary here, but serves as documentation on the type of the Java object that we're expecting. It could also serve as the basis for an implementation providing a way to get at the info contained in the access token.

We will also be using a piece of state stored in the surrounding Uno object (FacebookLogin). This state is a Java.Object _callbackmanager containing a CallbackManager from the Facebook SDK. We will show how this state is initialised in the next section.

We are now ready to implement the body of the Login method in Java.

To start with, we use a macro to get the callbackManager from the _this object:

@{
	CallbackManager callbackManager = (CallbackManager)@{FacebookLogin:Of(_this)._callbackManager:Get()};
	...
@}

The _this object in foreign code corresponds to the Uno object that the method is defined in (assuming it's not a static method). In words, the macro (the @{...} part) says something like "get the _callbackManager field of _this, which is an Uno object of type FacebookLogin" (see the foreign code documentation for more details). Java.Objects in Uno are converted into plain Java Objects on the Java side, so we cast it to CallbackManager, the type that we know that it has.

Next, we register our callbacks with the Facebook LoginManager:

LoginManager.getInstance().registerCallback(callbackManager,
	new FacebookCallback<LoginResult>()
	{
		@Override
		public void onSuccess(LoginResult loginResult)
		{
			AccessToken accessToken = loginResult.getAccessToken();
			Object unoAccessToken = @{AccessToken(Java.Object):New(accessToken)};
			onSuccess.run(unoAccessToken);
		}

		@Override
		public void onCancel()
		{
			onCancelled.run();
		}

		@Override
		public void onError(FacebookException exception)
		{
			onError.run(exception.toString());
		}
	}
);

In the onSuccess method, we first get the AccessToken from the LoginResult, and then create an Uno AccessToken (the class that we defined above), unoAccessToken, from it using the UXL :New macro, which results in an UnoObject. We get an UnoObject for Uno object when we are working on the Java side. UnoObject is somewhat like the opposite of Java.Object in Uno: it is a way to hold on to and pass around Uno objects in Java.

There is special support for passing delegates to Foreign methods. If the delegate is an Uno Action, it is automatically converted to a runnable in Java. If the delegate has parameters or returns a value, it's converted to a custom class with a run method that matches the signature of the delegate, only with the arguments and return types converted to their Java equivalent. So in the onSuccess method, we simply call the onSuccess delegate by using the run method, passing it the unoAccessToken that we just created.

We handle the onCancel and onError cases similarly. Note that the Java String used as an argument to onError.run is automatically converted to an Uno string when the function is invoked.

Setup

Initialization

The Facebook documentation instructs us to perform calls at various stages in the lifecycle of our app. The lifecycle of Fuse applications is not handled exactly the same way as it is in normal Android apps, so we can't do this exactly like in Facebook's documentation.

First, we hook our FacebookLogin class up to get callbacks when the relevant events are triggered. This is achieved by using the Lifecycle class in Fuse.Platform. In the constructor of FacebookLogin, we do:

public FacebookLogin()
{
	Lifecycle.Started += Started;
	Lifecycle.EnteringInteractive += OnEnteringInteractive;
	Lifecycle.ExitedInteractive += OnExitedInteractive;
}

Our implemententation of Started looks like this:

[Foreign(Language.Java)]
extern(Android) void Started(ApplicationState state)
@{
	FacebookSdk.sdkInitialize(Activity.getRootActivity());
	final CallbackManager callbackManager = CallbackManager.Factory.create();
	@{FacebookLogin:Of(_this)._callbackManager:Set(callbackManager)};
	Activity.subscribeToResults(new Activity.ResultListener()
	{
		@Override
		public boolean onResult(int requestCode, int resultCode, Intent data)
		{
			return callbackManager.onActivityResult(requestCode, resultCode, data);
		}
	});
@}

On the first line we use Activity.getRootActivity() to get the Activity of the application. This method comes from a Fuse-specific class called com.fuse.Activity. Next, we create a CallbackManager (from the Facebook SDK), and use a UXL macro to save its value in the _this object for later use. We can't directly override the onActivityResult method in Fuse as the Facebook SDK documentation says, but there is a hook, again in com.fuse.Activity, that we can add a listener to to get those callbacks. This is what the subscribeToResults call does.

Imports and Gradle dependencies

The foreign Java code in an Uno class all gets compiled to the same file. If the Java code needs imports, which it does in this case, we can use the ForeignInclude attribute on that class to add it to the generated Java file. For our FacebookLogin class, it looks like this:

[ForeignInclude(Language.Java, "android.content.Intent")]
[ForeignInclude(Language.Java, "com.facebook.*")]
[ForeignInclude(Language.Java, "com.facebook.appevents.AppEventsLogger")]
[ForeignInclude(Language.Java, "com.facebook.login.*")]
[ForeignInclude(Language.Java, "com.fuse.Activity")]
[ForeignInclude(Language.Java, "com.fusefacebook.ActivityResultListener")]
public class FacebookLogin
{
...
}

We additionally need to add the Facebook SDK itself. Uno supports the Gradle build system on Android, provided we build with the -DGRADLE flag, and generates a Gradle build file each time the app is built. We can instruct the compiler to add items to the dependencies and repositories sections by adding the following attributes to the class:

[Require("Gradle.Dependency","compile('com.facebook.android:facebook-android-sdk:4.8.+') { exclude module: 'support-v4' }")]
[Require("Gradle.Repository","mavenCentral()")]

Changing the Android manifest

The Facebook SDK requires some changes to the AndroidManifest.xml file. This is another file that Uno generates when we build the app. We can get changes added to it in a way similar to how we added the Gradle dependencies. This time the changes are a bit too big to comfortably add as an attribute, so we will be using an .uxl file to to specify them, which looks like this:

<Extensions Backend="CPlusPlus">
  <Set Facebook.AppID="YOUR-FACEBOOK-APP-ID" />
  <Require Condition="Android" Android.ResStrings.Declaration>
    <![CDATA[
      <string name="FACEBOOK_APP_ID">@(Facebook.AppID)</string>
    ]]>
  </Require>
  <Require Condition="Android" AndroidManifest.ApplicationElement>
    <![CDATA[
      <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/FACEBOOK_APP_ID" />
      <activity android:name="com.facebook.FacebookActivity"
              android:configChanges=
                     "keyboard|keyboardHidden|screenLayout|screenSize|orientation"
              android:theme="@android:style/Theme.Translucent.NoTitleBar"
              android:label="com.uno.test" />
    ]]>
  </Require>
</Extensions>

This first line creates a variable with our Facebook application ID, which we refer to later with @(Facebook.AppID).

We also Require a few elements, which basically means that we inform the Uno compiler about things that we want to add in specific places in the generated build files. For example, the AndroidManifest.ApplicationElement element allows you to add a child to the application element of the AndroidManifest.xml. For more information about these kinds of settings, see the build settings documentation.

iOS

We'll be following the Facebook login iOS guide closely, so, again, if you want to follow along in your own code, make sure that you have gone through their prerequisites. Here we will be using an FBSDKLoginManager to initiate login by pressing a Fuse button.

Triggering and responding to login

We use the same hooks as for our Android implementation to get lifecycle events. The iOS implementation is a little bit easier, because we do not need to store any state in the surrounding Uno object.

The login method is implemented using foreign Objective-C as follows:

[Foreign(Language.ObjC)]
public extern(iOS) void Login(Action<AccessToken> onSuccess, Action onCancelled, Action<string> onError)
@{
	FBSDKLoginManager* login = [[FBSDKLoginManager alloc] init];
	[login
		logInWithReadPermissions: @[@"public_profile"]
		fromViewController: [[[UIApplication sharedApplication] keyWindow] rootViewController]
		handler: ^(FBSDKLoginManagerLoginResult* result, NSError* error)
		{
			if (error)
			{
				onError([error localizedDescription]);
				return;
			}
			if (result.isCancelled)
			{
				onCancelled();
				return;
			}
			id<UnoObject> unoAccessToken = @{AccessToken(ObjC.Object):New(result.token)};
			onSuccess(unoAccessToken);
		}
	];
@}

We first create an FBSDKLoginManager and initiate login using that, calling back to the given delegates as appropriate.

Uno delegates passed to Objective-C are automatically converted to blocks with the corresponding signature, so they can be called like ordinary functions on the Objective-C side. The string argument to the onError callback is automatically converted from an NSString*, so we use [error localizedDescription] to get the error string in the case of an error and pass that to the callback.

We again use the UXL :New macro to create an Uno AccessToken object used in the onSuccess callback. We can hold on to Uno objects in Objective-C using the type id<UnoObject>.

Setup

Initialization

The initialization on iOS is similar to that on Android, only using the Objective-C interface to the SDK.

In the application Started hook, we do the following:

[Foreign(Language.ObjC)]
extern(iOS) void Started(ApplicationState state)
@{
	[[FBSDKApplicationDelegate sharedInstance]
		application: [UIApplication sharedApplication]
		didFinishLaunchingWithOptions: nil];
@}

Similarly, we invoke activateApp when entering interactive:

[Foreign(Language.ObjC)]
static extern(iOS) void OnEnteringInteractive(ApplicationState state)
@{
	[FBSDKAppEvents activateApp];
@}

On iOS we also need to pass received URIs to the Facebook SDK for it to know when a login is successful. We can hook onto such events using Fuse.Platform.InterApp.ReceivedURI, so we add to the constructor:

public FacebookLogin()
{
	...
	InterApp.ReceivedURI += OnReceivedUri;
}

When we get a callback with an URI that starts with fb, we pass it on to Facebook:

static void OnReceivedUri(string uri)
{
	if (uri.StartsWith("fb"))
	{
		OpenFacebookURL(uri);
	}
}

[Foreign(Language.ObjC)]
static extern(iOS) void OpenFacebookURL(string url)
@{
	[[FBSDKApplicationDelegate sharedInstance]
		application: [UIApplication sharedApplication]
		openURL: [NSURL URLWithString:url]
		sourceApplication: @"com.apple.mobilesafari"
		annotation: nil];
@}

Adding the framework and getting the imports right

This assumes that you have downloaded and extracted the Facebook SDK for iOS in a folder called FacebookSDKs-iOS in the folder that the Uno project is in.

Uno generates an Xcode project each time we build for iOS. To get the Facebook SDK framework added to the project, we add the following attributes to the Uno class:

[Require("Xcode.FrameworkDirectory", "@('FacebookSDKs-iOS':Path)")]
[Require("Xcode.Framework", "@('FacebookSDKs-iOS/FBSDKCoreKit.framework':Path)")]
[Require("Xcode.Framework", "@('FacebookSDKs-iOS/FBSDKLoginKit.framework':Path)")]
public class FacebookLogin
{
...
}

We use the @('relative/path':Path) syntax to specify a path relative to the current file. We do this since the default is to look for frameworks in the iPhone SDK system path (which only works for the frameworks that Apple bundle with the SDK). We also need to require the Xcode.FrameworkDirectory to ensure that Xcode looks for headers in that directory.

(As a side note, it's also possible to embed frameworks by using [Require("Xcode.EmbeddedFramework", "...")]. For more information about these kinds of settings, see the build settings documentation.)

Next, we need to make sure that the file generated from our Uno class that uses the Facebook SDK in foreign code has the required includes. We can achieve that like this since we're using entities from CoreKit and LoginKit:

[ForeignInclude(Language.ObjC, "FBSDKCoreKit/FBSDKCoreKit.h")]
[ForeignInclude(Language.ObjC, "FBSDKLoginKit/FBSDKLoginKit.h")]

Plists

The Facebook SDK requires us to add some elements to our Xcode project's plist file. Since that is generated on build, we do this in UXL, in the same file that we used to add elements to the AndroidManifest:

<Require Condition="iOS" Xcode.Plist.Element>
<![CDATA[
  <key>CFBundleURLTypes</key>
  <array>
    <dict>
      <key>CFBundleURLSchemes</key>
      <array>
        <string>fb@(Facebook.AppID)</string>
      </array>
    </dict>
  </array>
  <key>FacebookAppID</key>
  <string>@(Facebook.AppID)</string>
  <key>FacebookDisplayName</key>
  <string>Fuse Test App</string>

  <key>NSAppTransportSecurity</key>
  <dict>
    <key>NSExceptionDomains</key>
    <dict>
      <key>facebook.com</key>
      <dict>
        <key>NSIncludesSubdomains</key> <true/>
        <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/>
      </dict>
      <key>fbcdn.net</key>
      <dict>
        <key>NSIncludesSubdomains</key> <true/>
        <key>NSThirdPartyExceptionRequiresForwardSecrecy</key>  <false/>
      </dict>
      <key>akamaihd.net</key>
      <dict>
        <key>NSIncludesSubdomains</key> <true/>
        <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/>
      </dict>
    </dict>
  </dict>

  <key>LSApplicationQueriesSchemes</key>
  <array>
    <string>fbauth2</string>
  </array>
  ]]>
</Require>

Get the full project

The full project which works on both iOS and Android can be downloaded here.

The full project also hooks up the login functionality in a NativeModule such that the login function is available from JavaScript, returning a promise.