Updated April 20, 2016

React Native Meteor: OAuth with Facebook

Originally publish on medium.com.

This is a bit more advanced of a tutorial — if you’re going through this tutorial you should already be comfortable establishing a DDP connection from React Native to Meteor. In this example I’ll be using react-native-meteor, it’s still a work in progress but will work for our purposes in this tutorial. I’ll be skipping over configuration of that, if you need to follow the introduction here.

You can see all of this code here: https://github.com/spencercarli/react-native-meteor-accounts

Meteor

Before we get into anything else we’ll take care of the “easy” step — wiring Facebook OAuth in the browser. This will let us set up the project and get that out of the way so we can really focus on the React Native parts.

This has been written about multiple times so if you get stuck please refer to some of the various articles on this subject. My reference is this guide put together by The Meteor Chef. We’re covering it here again because it shows us that the code we’re writing for React Native will still work for the browser.

Once you’ve created your Meteor app you’ll want to add a few packages.

meteor add accounts-facebook service-configuration accounts-ui

Once that’s done create a settings.json file in your Meteor app directory. We’ll used that to store our Facebook info in a moment. To follow along with the following snippets use this code structure.

settings.json

{
  "oauth": {
    "facebook": {
      "appId": "",
      "secret": ""
    }
  }
}

Creating the Facebook App

First, we want to create a new app at this link. For right now just start with creating for a website. We’ll add the platform dependent ones to our Facebook app later. Go ahead and skip the “quick start”.

That should drop you off here:

0

Go ahead and copy the “App ID” and place it in the appId field in settings.json. Do the same for “App Secret”. Okay, that should be done now.

Connecting Meteor

Now we need to hook up our app. I’m using Meteor 1.3 conventions in these code snippets. First on our server we’ll create server/imports/oauth-facebook.js with the following code

server/imports/oauth-facebook.js

import { ServiceConfiguration } from 'meteor/service-configuration';
import { Meteor } from 'meteor/meteor';

const settings = Meteor.settings.oauth.facebook;

const init = () => {
  if (!settings) return;
  ServiceConfiguration.configurations.upsert(
    { service: 'facebook' },
    {
      $set: {
        appId: settings.appId,
        secret: settings.secret,
      },
    }
  );
};

export default init;

After we import the necessary code we’re grabbing our settings info. If the settings don’t exist then we’re not going to try to insert anything. If we’ve got the data though we’ll store that in the ServiceConfiguration collection. This will be used by accounts-facebook for Meteor and our code later.

Since this is 1.3 we’ve got to import our new code into the server entry file.

server/main.js

import { Meteor } from 'meteor/meteor';
import FacebookOAuthInit from './imports/oauth-facebook';

Meteor.startup(() => {
  // code to run on server at startup
  FacebookOAuthInit();
});

Last thing we’ll do on the Meteor side is setup our accounts-ui buttons

client/main.html

<head>
  <title>simple</title>
</head>

<body>
  <h1>Welcome to Meteor!</h1>

  {{> loginButtons}}
</body>

This should set up up with this wonderful UI

1

Okay, now onto the new stuff.

React Native

Let’s cover the general React Native stuff first. We’ll be leveraging React Native FBSDK, a wrapper around the iOS and Android SDK.

Also you’ll want to set up a React Native project. Here’s a super brief guide on a way to get started:

react-native init RNFacebookExample && cd RNFacebookExample

Then follow the directions here to install react-native-meteor. Then

mkdir app && cd app/ && touch index.js

As a temporary solution you can use this in app/index.js

app/index.js

import React, { Component, StyleSheet, Text, View } from 'react-native';
import Meteor, { createContainer } from 'react-native-meteor';

const url = 'http://localhost:3000/websocket';
Meteor.connect(url);

class App extends Component {
  render() {
    if (this.props.user) {
      return (
        <View style={styles.container}>
          <Text>You are signed IN</Text>
        </View>
      );
    }

    return (
      <View style={styles.container}>
        <Text>You are signed OUT</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
});

export default createContainer(() => {
  return {
    user: Meteor.user(),
  };
}, App);

Finally, change index.ios.js and index.android.js to

index.*.js

import React, { AppRegistry } from 'react-native';
import App from './app';

AppRegistry.registerComponent('RNFacebookExample', () => App);

Okay, you should now be good to do platform specific stuff. Feel free to only do the platform you’re interested in — the other isn’t required.

Also, as a prerequisite to the platform code we’ll need to install the actual SDK.

react-native install --save react-native-fbsdk
react-native link react-native-fbsdk

React Native iOS Setup

First step we want to take is add iOS support to our Facebook app. First go to the list of your apps and choose the one you’re using to follow along.

Then click the “Settings” tab, that should drop you off at a screen like so:

2

Now at the bottom of this page you’ll want to click “Add Platform” and select iOS.

Now you’ve got to add your app’s Bundle ID and enable Single Sign On. You can get your Bundle ID from XCode in the “General” tab. Your Facebook configuration should look something like this now.

3

Save your changes and either leave this tab open or copy your Facebook App ID and App Name. If you get stuck please reference the Facebook docs here.

Okay, now to our app…

I’m replicating the docs created here so if you run into issues please reference those instructions (they may change over time).

Now we want to download the Facebook SDK for iOS (that link will bring you to Facebook’s developer site, it won’t download some random zip file). Once you’ve got that unzip it and open it in Finder.

Open your React Native app’s Xcode project (ios/APP_NAME.xcodeproj) and with that open you want to drage and drop the following files from the FacebookSDK to the Frameworks group in Xcode — Bolts.framework, FBSDKCoreKit.framework, FBSDKLoginKit.framework, FBSDKShareKit.framework. Make sure to choose Create groups for any added folders and selected Copy items into destination group’s folder.

Now we need to configure our Info.plist in a way that the Facebook SDK can read the necessary values. This is following along with the instructions here.

In XCode we need to open our Info.plist and open it as source code.

4

Then add the following snipped before . Sub out your-app-id and your-app-name with the appropriate values for your application.

<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>fb{your-app-id}</string>
</array>
</dict>
</array>
<key>FacebookAppID</key>
<string>{your-app-id}</string>
<key>FacebookDisplayName</key>
<string>{your-app-name}</string>

5

iOS 9 Support

This part tripped me up so make sure to do it. iOS made some changes in iOS 9 so we need to whitelist a few Facebook apps (read more here). Much like above we have to add the following.

<key>LSApplicationQueriesSchemes</key>
<array>
<string>fbapi</string>
<string>fb-messenger-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
</array>
<key>NSPhotoLibraryUsageDescription</key>
<string>{human-readable reason for photo access}</string>

Is this taking as long to setup as it is to write? Geez…

iOS10 Support

It’s been brought to my attention by Benjamin Cherion that in iOS10 you need to enable Keychain Sharing in Xcode.

6

Now you need to add a few things to your AppDelegate.m — the import will go at the top, near the other imports and the rest at the end, but before the end

AppDelegate.m

#import <FBSDKCoreKit/FBSDKCoreKit.h>

// Removed for brevity

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  [[FBSDKApplicationDelegate sharedInstance] application:application
    didFinishLaunchingWithOptions:launchOptions];
  // Add any custom logic here.
  return YES;
}

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {

  BOOL handled = [[FBSDKApplicationDelegate sharedInstance] application:application
    openURL:url
    sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
    annotation:options[UIApplicationOpenURLOptionsAnnotationKey]
  ];
  return handled;
}

@end

Should be set up now! Now is Android :)

React Native Android Setup

First thing we have to do is configure our Facebook app for Android. These steps are based off this documentation.

I’m not super familiar with Android so I’ll be going through the Quick Start guide. You can find that here (for android). We’ll be configuring the SDK in a moment so just skip ahead to the Add Facebook App ID section. You should see something like this:

7

With that let’s add our facebook app id. Make sure to use your app id.

android/app/src/main/res/values/strings.xml

<resources>
    <string name="app_name">RNFacebookExample</string>

    <string name="facebook_app_id">262456924101170</string>
</resources>

Then we need to setup android/app/src/main/AndroidManifest.xml. We’re confirming that

<uses-permission android:name=”android.permission.INTERNET” />

is in the file and adding

<meta-data android:name=”com.facebook.sdk.ApplicationId” android:value=”@string/facebook\_app\_id”/>

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.rnfacebookexample">

    <uses-permission android:name="android.permission.INTERNET" /> <!-- ADDED -->

    <application
      android:allowBackup="true"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:theme="@style/AppTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/> <!-- ADDED -->
      </activity>
      <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
    </application>

</manifest>

Now we need to add info to Facebook (again in their Quick Start guide. This info is coming from the following line in AndroidManifest.xml

<manifest xmlns:android=”http://schemas.android.com/apk/res/android" package=”com.rnfacebookexample”>

8

Since we’re not in the app store we can skip over this warning

9

We then have to create a development key hash like so

keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64

Add your new hash in the field. Each development machine will need their own hash. Note: You’ll have to do the same for a release hash.

10


Okay, now that we’ve got that side setup let’s setup our app to actually handle this stuff.

We’ve got to handle various imports to our MainActivity.java file, I’ve just handled them all here. You can view more details in the packages docs.

The following instructions will only work if you’re on React Native version 0.29 or greater, if below make sure to check out the official docs for the old instructions.

In your MainApplication.java file we need to import a few files and then create an instance variable of type CallbackManager. Then, we need to override the onCreate method, and finally register the sdk package in getPackages.

MainApplication.java

import com.facebook.CallbackManager;
import com.facebook.FacebookSdk;
import com.facebook.reactnative.androidsdk.FBSDKPackage;
import com.facebook.appevents.AppEventsLogger;

...

public class MainApplication extends Application implements ReactApplication {

  private static CallbackManager mCallbackManager = CallbackManager.Factory.create();

  protected static CallbackManager getCallbackManager() {
    return mCallbackManager;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    FacebookSdk.sdkInitialize(getApplicationContext());
  }

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    protected boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
          new FBSDKPackage(mCallbackManager)
      );
    }
  };
  //...
};

Then in MainActivity.java you need to override the onActivityResult method.

MainActivity.java

import android.content.Intent;

public class MainActivity extends ReactActivity {

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        MainApplication.getCallbackManager().onActivityResult(requestCode, resultCode, data);
    }
    //...
}

You should now be setup on Android! Let’s get to setting up the UI now.

React Native UI

Now that all of the configuration is setup let’s make use of it! We’ll have 100% code reuse in this aspect of our app. Let’s create some separate views, just to make things more obvious in a little bit. Run these commands from within the RNFacebookExample/app directory.

touch SignIn.js SignOut.js

then in those files

app/SignIn.js

import React, { Component, StyleSheet, Text, View } from 'react-native';

class SignIn extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Sign In View</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
});

export default SignIn;

app/SignOut.js

import React, { Component, StyleSheet, Text, View } from 'react-native';

class SignOut extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Sign Out View</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
});

export default SignedOut;

then let’s update app/index.js to use our new components*.*

app/index.js

import React, {
  Component,
  StyleSheet,
  Text,
  View,
  AsyncStorage,
} from 'react-native';
import Meteor, { connectMeteor } from 'react-native-meteor';
import SignIn from './SignIn';
import SignOut from './SignOut';

// Removed for brevity

render() {
  if (this.props.user) {
    return <SignOut />
  }
  return <SignIn />;
}

// Removed for brevity

Okay let’s add a Facebook login button! This code will live in app/SignIn.js. First we’ll import the Facebook SDK, including the LoginButton component. This component will give us the design of the button for free and we just need to pass in a few props. This includes the permissions we’re asking for and what to do when login finishes and, if needed, what to do when logout finishes.

SignIn.js

// Removed for brevity

import FBSDK, { LoginButton } from 'react-native-fbsdk';

const onLoginFinished = (error, result) => {
  if (error) {
    alert('login has error: ' + result.error);
  } else if (result.isCancelled) {
    alert('login is cancelled.');
  } else {
    alert('login has finished with permissions: ' + result.grantedPermissions);
  }
};

class SignIn extends Component {
  render() {
    return (
      <View style={styles.container}>
        <LoginButton
          publishPermissions={['publish_actions']}
          onLoginFinished={onLoginFinished}
          onLogoutFinished={() => alert('logout.')}
        />
      </View>
    );
  }
}

// Removed for brevity

So what are we going to do with this? We’ll pass this along to our Meteor backend and either create a user, if one doesn’t already exist, or log that user in, if one does exist. We’ll tap into the standard Meteor accounts system for this.

First thing we need to do is setup a new login handler on the Meteor server. This code will take place in our Meteor app in server/imports/oauth-facebook.js.

A quick overview of what we’re doing: Following the ServiceConfiguration (which I covered earlier) we’re registering a new login handler for Meteor. This allows us to call Meteor.call(login, {facebook: data}); (where data is what we get back from the Facebook SDK. It then tries to fetch data about the user from Facebook — this serves two purposes. 1 it checks the validity of the access token that was passed to our application and 2 it grabs some information for us so that we can build out the profile for the user. Lastly it checks if that user already exists in the database, if so then we log them in and update their info otherwise we create a new user for them. The last thing we have to do when registering a login handler is return an object with the userId.

server/imports/oauth-facebook.js

import { ServiceConfiguration } from 'meteor/service-configuration';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { HTTP } from 'meteor/http';
import { _ } from 'meteor/underscore';

const settings = Meteor.settings.oauth.facebook;

const init = () => {
  if (!settings) return;
  ServiceConfiguration.configurations.upsert(
    { service: 'facebook' },
    {
      $set: {
        appId: settings.appId,
        secret: settings.secret,
      },
    }
  );

  registerHandler();
};

const registerHandler = () => {
  Accounts.registerLoginHandler('facebook', function (params) {
    const data = params.facebook;

    // If this isn't facebook login then we don't care about it. No need to proceed.
    if (!data) {
      return undefined;
    }

    // The fields we care about (same as Meteor's)
    const whitelisted = [
      'id',
      'email',
      'name',
      'first_name',
      'last_name',
      'link',
      'gender',
      'locale',
      'age_range',
    ];

    // Get our user's identifying information. This also checks if the accessToken
    // is valid. If not it will error out.
    const identity = getIdentity(data.accessToken, whitelisted);

    // Build our actual data object.
    const serviceData = {
      accessToken: data.accessToken,
      expiresAt: +new Date() + 1000 * data.expirationTime,
    };
    const fields = Object.assign({}, serviceData, identity);

    // Search for an existing user with that facebook id
    const existingUser = Meteor.users.findOne({
      'services.facebook.id': identity.id,
    });

    let userId;
    if (existingUser) {
      userId = existingUser._id;

      // Update our data to be in line with the latest from Facebook
      const prefixedData = {};
      _.each(fields, (val, key) => {
        prefixedData[`services.facebook.${key}`] = val;
      });

      Meteor.users.update(
        { _id: userId },
        {
          $set: prefixedData,
          $addToSet: { emails: { address: identity.email, verified: true } },
        }
      );
    } else {
      // Create our user
      userId = Meteor.users.insert({
        services: {
          facebook: fields,
        },
        profile: { name: identity.name },
        emails: [
          {
            address: identity.email,
            verified: true,
          },
        ],
      });
    }

    return { userId: userId };
  });
};

// Gets the identity of our user and by extension checks if
// our access token is valid.
const getIdentity = (accessToken, fields) => {
  try {
    return HTTP.get('https://graph.facebook.com/v2.4/me', {
      params: {
        access_token: accessToken,
        fields: fields,
      },
    }).data;
  } catch (err) {
    throw _.extend(
      new Error('Failed to fetch identity from Facebook. ' + err.message),
      { response: err.response }
    );
  }
};

export default init;

This code could likely/should live in a package. During this transition period for Meteor I thought I would post the source here so you can implement it however works for your application.

Now that the Meteor side of things is set up to accept our Facebook login. We’ll be writing our FB related logic in a separate file to keep things cleaner and abstract what we can. This taps into a fair amount of react-native-meteor internals but it’s functional-I’ll update this as new/better patterns emerge.

app/fb-login.js

import React, { AsyncStorage } from 'react-native';
import { AccessToken } from 'react-native-fbsdk';
import Meteor from 'react-native-meteor';

const USER_TOKEN_KEY = 'reactnativemeteor_usertoken';

export const loginWithTokens = () => {
  const Data = Meteor.getData();
  AccessToken.getCurrentAccessToken().then((res) => {
    if (res) {
      Meteor.call('login', { facebook: res }, (err, result) => {
        if (!err) {
          //save user id and token
          AsyncStorage.setItem(USER_TOKEN_KEY, result.token);
          Data._tokenIdSaved = result.token;
          Meteor._userIdSaved = result.id;
          Meteor._loginWithToken(result.token);
        }
      });
    }
  });
};

export const onLoginFinished = (error, result) => {
  if (error) {
    console.log('login error', error);
  } else if (result.isCancelled) {
    console.log('login cancelled');
  } else {
    loginWithTokens();
  }
};

In short we’re setting up a function that we can pass as a callback to our LoginButton button component that, if login was successful, will log us into Meteor. It’s also exposing a function that we can call when our component mounts to try and log us in if we don’t have a Meteor resume token but we do have a Facebook access token.

Now lets wire all this up and be done here.

First thing we’ll want to do is import our loginWithTokens function from fb-login.js. What we want to do is attempt to log the user in with their Facebook access token as soon as we have a connection. Note: react-native-meteor already logs a user in behind the scenes this function is serving as a backup incase the resume tokens expired but the user is still logged in with Facebook.

app/index.js

// Removed for brevity

import { loginWithTokens } from './fb-login';

const url = 'http://localhost:3000/websocket';
Meteor.connect(url);

class App extends Component {
  // Removed for brevity

  componentWillMount() {
    loginWithTokens();
  }

  // Removed for brevity
}

// Removed for brevity

Since written a new onLoginFinished function in fb-login.js we’ll delete the existing one in SignIn.js and import our new one. That’s really all the change we have to do here.

app/SignIn.js

// Removed for brevity

import FBSDK, { LoginButton } from 'react-native-fbsdk';
import { onLoginFinished } from './fb-login';

class SignIn extends Component {
  render() {
    return (
      <View style={styles.container}>
        <LoginButton
          publishPermissions={['publish_actions']}
          onLoginFinished={onLoginFinished}
          onLogoutFinished={() => alert('logout.')}
        />
      </View>
    );
  }
}

// Removed for brevity

Finally, the user should be able to sign out! Let’s give them that ability. All we’re doing here is creating a touchable area and when that area is touched we call react-native-meteor’s logout method.

app/SignOut.js

import React, {
  Component,
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
} from 'react-native';
import Meteor from 'react-native-meteor';

class SignOut extends Component {
  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity
          style={styles.buttonContainer}
          onPress={() => Meteor.logout()}
        >
          <Text style={styles.buttonText}>Sign Out</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  buttonContainer: {
    backgroundColor: '#3B5998',
    paddingHorizontal: 15,
    paddingVertical: 10,
    borderRadius: 10,
  },
  buttonText: {
    color: '#FFFFFF',
    fontWeight: '500',
  },
});

export default SignOut;

You should now be able to run your app on Android via react-native run-android and on iOS via npm run ios. Note: If you’re running on a device (or an Android emulator) you’ll have to change localhost in app/index.js to your machine’s IP address.

DONEZO.

You’ll noticed that it’s also got a logout with Facebook button. You should handle this however makes sense for your application — explore the Facebook React Native SDK for more info on that.

That was a beast to write. I hope you found it valuable. If so, please recommend this post, help others minimize the pain associated with setting this up!

React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our courses and our private Slack community.

Learn More