Building a location sharing app with React Native and Pusher

location-sharing-react-native-pusher-header.png

Learn how to build a location-sharing app using the geolocation functionality in React Native and broadcast geolocation data in realtime with Pusher.

Introduction

In this tutorial, we’ll be building a location-sharing app with React Native and Pusher. By reading this tutorial, you will learn how to use the geolocation functionality in React Native, and broadcast the geolocation data with Pusher. You will also learn how to integrate Facebook login into the app.

Prerequisites

You will need the following in order to follow the tutorial:

  • React Native development environment – if you don’t have a machine setup for React Native development, be sure to check out the official docs on how to get started.
  • Genymotion Emulator – this is used for testing the app later on. You can actually use your Android smartphone as well, but Genymotion really makes it easy to test geolocation using their GPS Emulator.
  • Pusher app – you’ll need to create a Pusher account in order to use Pusher’s services. A Pusher account is free to create and it even provides you with ample resources for testing the service. Once you have an account, create an app which will be used for connecting to Pusher.
  • Facebook app – this is required because we’ll be using the Facebook login for the app.
  • Google project – this is required because Google Map is used for the map display.

In the following sections, I’ll be showing you how to create the Pusher, Facebook, and Google project.

Creating the Pusher App

Once you’re logged in to your Pusher account, go to your Dashboard and look for the menu for creating a new app. Set the name of the app to “locSharer”, and select the cluster nearest to your location. Click on the Create my app button to create the app. Once the app is created, click on the App Settings tab and enable Client Events. We need this because we’ll be sending events directly from the app. After that, click on the App Keys tab and copy the credentials somewhere where you can easily access it later on. We’ll be needing it later once we start configuring the app.

Creating the Facebook App

The minimum requirement for creating a Facebook app is for you to have a Facebook account. Once you’re logged in to your account, go to the Facebook developers website and create a new app. Set the Display Name to “locSharer”. Once the app is created, add Android as a platform then set the following details:
Google Play Package Name: com.locsharer
Class Name: com.locsharer.MainActivity

Next, generate a key hash to be used for development:

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

The command above generates a 28-character key hash. Paste the value under the Key Hashes field and save the changes. This step ensures the authenticity of the interactions between your app and Facebook, thus it’s a required step even for the development environment. You can find more information about this on the Facebook login documentation for Android.

Creating the Google Project

Just like Facebook, you need to have a Google account in order to create a Google project. Once you’re logged in to your Google account, go to the Google Developer Console and create a project. Set the project name to “locSharer”. Once the project is created, click on Enable APIs and Services button. From there, look for Google Maps Android API and enable it. Next, click on the Credentials tab and create an API key. Once the key is created, it will ask you to restrict access. Set the key restriction to Android. Then you can use the same keystore you used for Facebook:

1keytool -list -v -keystore ~/.android/debug.keystore

The command above allows you to get the sha1 hash. Look for it, copy the corresponding value and paste it under the SHA-1 certificate fingerprint field. Also, enter the package name of the app (com.locsharer) then save the changes.

App Overview

As mentioned earlier, we will be creating a location-sharing app. First the user has to login with their Facebook account:

Once logged in, the user can enable location-sharing so that their friends can see their current location when they view them:

If the user has friends who are also using the app, they will be listed below the user’s details. Tapping on a friend will display a map which gets updated based on their current location (but only if they have enabled location sharing). The current location is indicated by a marker:

You can find the source code for this project on its Github repo.

Creating the Server

Just like every other Pusher app integration, this app needs a server component as well. The server’s job is to authenticate the requests coming from the app. This allows us to make sure that the request is indeed coming from the app and not anywhere else.

Start by creating a new folder for the server-related files. Inside the folder, run npm init to initialize a new project. Simply press enter until it asks you to confirm the project details. Once you get to that, just respond with yes.

Next, install the packages that we’ll be needing:

1npm install --save express body-parser pusher

Once the packages are installed, create a server.js file. Start by including the packages we just installed:

1var express = require('express');
2    var bodyParser = require('body-parser');
3    var Pusher = require('pusher');
4
5    var app = express();
6    app.use(bodyParser.json());
7    app.use(bodyParser.urlencoded({ extended: false }));

Next, add the code for connecting to Pusher. The Pusher app credentials are being loaded as environment variables. As you have seen from the code below, we’re not really using a module for loading environment variables from a .env file. Later I’ll show you how the values are being supplied.

1var pusher = new Pusher({ 
2      appId: process.env.APP_ID, 
3      key: process.env.APP_KEY, 
4      secret:  process.env.APP_SECRET,
5      cluster: process.env.APP_CLUSTER, 
6    });

Add a route for verifying if the server is really working:

1app.get('/', function(req, res){ 
2      res.send('server is running');
3    });

Add the code for authenticating users that are connecting to your Pusher app. This contains the unique key that we will use later on to check whether the request has indeed come from the app.

1app.post('/pusher/auth', function(req, res) {
2      var socketId = req.body.socket_id;
3      var channel = req.body.channel_name;
4      var auth = pusher.authenticate(socketId, channel);  
5      var app_key = req.body.app_key;
6      if(app_key == process.env.UNIQUE_KEY){
7        var auth = pusher.authenticate(socketId, channel);
8        res.send(auth);
9      }
10
11      res.send(auth);
12    });

Initiate the server on the port set in the environment variables. Normally this would be served on port 80:

1var port = process.env.PORT || 5000;
2    app.listen(port);

Deploying the Server

The server needs to be accessible via the internet. One service that allows us to do this for free is Now. **You can install Now **globally with the following command:

1npm install -g now

Once installed, you can now add your Pusher app credentials as a secret. One caveat of Now is that all the files for the deployed projects are available publicly. This means that the values in the .env files are publicly available as well. Adding those values as a secret means that it won’t be accessible anywhere.

1now secret add locshare_app_id YOUR_PUSHER_APP_ID
2    now secret add locshare_app_key YOUR_PUSHER_APP_KEY
3    now secret add locshare_app_secret YOUR_PUSHER_APP_SECRET
4    now secret add locshare_app_cluster YOUR_PUSHER_APP_CLUSTER
5    now secret add locshare_unique_key YOUR_UNIQUE_KEY

Don’t forget to replace the values with your actual Pusher app credentials.

Once that’s done, you can deploy the server:

1now -e APP_ID=@locshare_app_id -e APP_KEY=@locshare_app_key -e APP_SECRET=@locshare_app_secret APP_CLUSTER=@locshare_app_cluster -e UNIQUE_KEY=@locshare_unique_key

What the command above does is deploy the server, as well as setting the environment variables using the -e option. The secret values that you’ve added earlier are accessed by using the @ sign. When the process is completed, it should return a URL pointing to the server. Access that in the browser to check whether the server is running or not.

Creating the App

It’s now time to create the actual app. Start by generating a new React Native project:

1react-native init LocSharer

Installing and Configuring the Dependencies

Next, install the dependencies of the app:

1npm install --save prop-types pusher-js react-native-facebook-login react-native-maps react-navigation

Here’s a brief overview of what each package does:
prop-types – for specifying the intended types of properties passed to components.
pusher-js – for interacting with Pusher.
react-native-facebook-login – for implementing Facebook login.
react-native-maps – for displaying Google Maps and markers.
react-navigation – for implementing Stack navigation within the app.

Additional steps are required in order for Facebook login and Google Maps to work. We’ll look at how to do that in the sections to follow.

Configuring Facebook Login

The following steps assume that you have already created a Facebook app. So create one, if you haven’t done so already.

Once you’ve created a Facebook app, open the android/settings.gradle file and add the following to the bottom of the file:

1include ':react-native-facebook-login'
2    project(':react-native-facebook-login').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-facebook-login/android')

Next, open the android/app/build.gradle file and add the following inside the dependencies:

1dependencies {
2      ...
3      compile project(':react-native-facebook-login')
4    }

Next, register the React package by opening the android/app/src/main/java/com/{YOUR PACKAGE NAME}/MainApplication.java file, and adding the following:

1// top of the file
2    import com.magus.fblogin.FacebookLoginPackage; // <--- add this
3
4    public class MainApplication extends Application implements ReactApplication {
5
6      ...
7
8      @Override
9      protected List<ReactPackage> getPackages() {
10          return Arrays.<ReactPackage>asList(
11              new MainReactPackage(),
12              new FacebookLoginPackage() // <--- add this
13          );
14      }
15
16      ...
17    }

Next, open the android/app/src/main/res/values/strings.xml file and add the details of the Facebook app you created earlier:

1<resources>
2      <string name="app_name">{YOUR FACEBOOK APP NAME}</string>
3      <string name="fb_app_id">{YOUR FACEBOOK APP ID}</string>
4      <string name="fb_login_protocol_scheme">fb{YOUR FACEBOOK APP ID}</string>
5    </resources>

Lastly, open the android/app/src/main/AndroidManifest.xml file and add the following:

1<manifest 
2      xmlns:android="http://schemas.android.com/apk/res/android"
3      xmlns:tools="http://schemas.android.com/tools" <-- add this
4      package="com.your.app.namespace">
5
6      <application
7        ...
8
9        <!--add FacebookActivity-->
10        <activity 
11          tools:replace="android:theme"
12          android:name="com.facebook.FacebookActivity"
13          android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation"
14          android:label="@string/app_name"
15          android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
16
17        <!--add CustomTabActivity-->
18        <activity
19          android:name="com.facebook.CustomTabActivity"
20          android:exported="true">
21          <intent-filter>
22            <action android:name="android.intent.action.VIEW" />
23            <category android:name="android.intent.category.DEFAULT" />
24            <category android:name="android.intent.category.BROWSABLE" />
25            <data android:scheme="@string/fb_login_protocol_scheme" />
26          </intent-filter>
27        </activity>
28
29        <!--add reference to Facebook App ID-->
30        <meta-data
31          android:name="com.facebook.sdk.ApplicationId"
32          android:value="@string/fb_app_id"/>
33
34      </application>
35    </manifest>

Configuring React Native Maps

The following steps assume that you have already created a Google project, and generated an API key.

Start by linking the package resources to your app:

1react-native link react-native-maps

Open the android\app\src\main\AndroidManifest.xml file and add a reference to your Google project’s API key:

1<application>
2      ...
3      <meta-data
4        android:name="com.google.android.geo.API_KEY"
5        android:value="YOUR GOOGLE PROJECT'S ANDROID API KEY"/>
6    </application>

Also, add the following below the default permissions:

1<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
2    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Coding the App

Now we’re ready to actually code the app. Start by opening the index.js file and replace the default contents with the following:

1import { AppRegistry } from 'react-native';
2    import App from './App';
3
4    AppRegistry.registerComponent('LocSharer', () => App);

The entry point of the app will be the App component. To create an App.js file and add the following:

1import React, { Component } from 'react';
2    import { StackNavigator } from 'react-navigation';
3
4    import IndexPage from './src/components/index';
5    import MapPage from './src/components/map_page';
6
7    const Page = StackNavigator({
8      Home: { screen: IndexPage },
9      MapPage: { screen: MapPage },
10    });
11
12    export default class App extends Component<{}> {
13
14      render() {
15        return <Page />
16      }
17    }

The code above uses the React Navigation library to create a StackNavigator. This allows the app to transition from one screen to another by placing the new screen on top of the stack. This allows us to easily implement the back functionality since all it has to do is to “pop” the current screen out of the stack in order to go back to the previous screen. To use the StackNavigator, pass in the components to be used as the individual pages. The first one is the initial screen of the app.

Index Page

Next, create a src directory and inside create an index.js file. This will serve as the initial page of the app. First, import the modules and components that we need:

1import React, { Component } from 'react';
2    import {
3      StyleSheet,
4      Text,
5      View,
6      Switch, // for toggling location sharing on and off
7      DeviceEventEmitter // for emitting/listening custom events
8    } from 'react-native';
9
10    var { FBLogin } = require('react-native-facebook-login'); // for implementing Facebook login
11
12    import Pusher from 'pusher-js/react-native'; // for interacting with Pusher
13
14    import Profile from './profile'; // component for displaying the user's profile
15    import Friends from './friends'; // component for displaying the user's friends
16
17    import { regionFrom } from '../helpers'; // helper function for constructing the data needed by React Native Maps

Create the actual component:

1export default class Index extends Component<{}> {
2      // set the title of the screen
3      static navigationOptions = {
4        title: 'LocSharer',
5      };
6    }

In the constructor, we bind the functions to be used throughout the class as well as setting the default state:

1constructor() {
2
3      super();
4
5      this.watchId = null; // unique ID for the geolocation watcher
6      this.pusher = null; // variable for storing the Pusher instance
7      this.user_channel = null; // the Pusher channel for the current user
8
9      // bind the functions to the class
10      this.onLogin = this.onLogin.bind(this);
11      this.onLoginFound = this.onLoginFound.bind(this);
12      this.onLogout = this.onLogout.bind(this);
13      this.setUser = this.setUser.bind(this);
14      this.setFriends = this.setFriends.bind(this);
15      this.toggleLocationSharing = this.toggleLocationSharing.bind(this);
16      this.onViewLocation = this.onViewLocation.bind(this);
17
18      this.state = {
19        is_loggedin: false, // whether the user is currently logged in or not
20        is_location_shared: false, // whether the user is currently sharing their location or not
21        user: null, // data for the currently logged in user
22        friends: null, // data for the user's friends
23        subscribed_to: null, // the Facbook user ID of the user's friend whose location is currently being viewed
24        subscribed_friends_count: 0 // number of friends currently subscribed to the user
25      };
26
27    }

The onLogin() function is executed when the user has logged in with Facebook. Some of the user’s data such as the ID, access token, and name are passed in as an argument to this function. It is then used to set the user’s and friends’ data on the state using two functions:

1onLogin(login_data) {
2      this.setUser(login_data);
3      this.setFriends(login_data.credentials.token);
4    }

The onLoginFound() function is executed if an existing Facebook session is already present. The arguments passed into this function are limited so we have to make a separate API request to get the user’s name:

1onLoginFound(data) {
2
3      let token = data.credentials.token;
4
5      fetch(`https://graph.facebook.com/me?access_token=${token}`)
6        .then((response) => response.json())
7        .then((responseJson) => {
8
9          let login_data = {
10            profile: {
11              id: responseJson.id,
12              name: responseJson.name
13            },
14            credentials: {
15              token: token
16            }
17          };
18
19          this.setUser(login_data);
20        })
21        .catch((error) => {
22          console.log('something went wrong', error);
23        });
24
25      this.setFriends(token);
26
27    }

Here’s the function for setting the data for the current user. All it does is the format the login data returned by the Facebook API and set it on the state:

1setUser(login_data) {
2
3      let user_id = login_data.profile.id;
4      this.setState({
5        is_loggedin: true,
6        user: {
7          id: user_id,
8          access_token: login_data.credentials.token,
9          name: login_data.profile.name,
10          photo: `https://graph.facebook.com/${user_id}/picture?width=100` // the user's profile picture
11        }
12      });
13
14    }

The setFriends() function makes a request to the Facebook API to get the array of the user’s friends:

1setFriends(token) {
2      fetch(`https://graph.facebook.com/me/friends?access_token=${token}`)
3        .then((response) => response.json())
4        .then((responseJson) => {
5          this.setState({
6            friends: responseJson.data
7          });
8        })
9        .catch((error) => {
10          console.log('something went wrong', error);
11        });
12    }

Once the user logs out, destroying the session data is already taken care of by the Facebook login package. So all we have to do is unset all the user data that we’ve set earlier:

1onLogout() {
2      this.setState({
3        is_loggedin: false,
4        user: null, 
5        friends: null,
6        is_subscribed_to: null
7      });
8    }

Next, initialize Pusher. Be sure to replace the placeholder values with your Pusher app details. We’re also passing in an auth parameter. This is the request data that we were checking earlier in the server code. Simply pass in the same unique string that you’ve used earlier when you added the secret:

1componentWillMount() {
2      this.pusher = new Pusher('YOUR PUSHER APP ID', {
3        authEndpoint: 'YOUR AUTH SERVER AUTH ENDPOINT',
4        cluster: 'YOUR APP CLUSTER',
5        encrypted: true,
6        auth: {
7          params: {
8            app_key: 'YOUR UNIQUE KEY', // <-- should be the same as the unique key you added as a secret using now
9          }
10        }
11      });
12
13      // add code for listening for the unsubscribe event
14
15    }

Next, we need a way to unsubscribe from a friend’s channel when the current user is no longer viewing their location on a map. That happens when the user goes back from the map page to the index page. The React Navigation library doesn’t really provide a way to listen for the event when the back button is tapped. That’s why we need a way to emulate that behavior. I’ll let you figure out your own solution. So feel free to skip the following paragraph if you want.

The solution I came up with is to use the DeviceEventEmitter module. Add a listener for the unsubscribe event, and once this is triggered, unsubscribe from the friend’s channel. The event is triggered from the map page when the user goes back to the index page:

1DeviceEventEmitter.addListener('unsubscribe', (e) => {
2      let friend_id = this.state.subscribed_to;
3      this.pusher.unsubscribe(`private-friend-${friend_id}`);
4    });

The toggleLocationSharing() function is executed every time the user toggles the switch for sharing their location. If location sharing is enabled, we subscribe the user to their own channel. This allows them to listen for when one of their friends subscribes to their channel. When this happens, we begin watching the user’s current location and publish the data using Pusher. If the user decides to disable location sharing, we unsubscribe the user from their own channel and stop watching the location. This effectively stops the updating of location from their friend’s screens:

1toggleLocationSharing() {
2
3      let is_location_shared = !this.state.is_location_shared;
4
5      this.setState({
6        is_location_shared: is_location_shared
7      });
8
9      let user_id = this.state.user.id;
10      if(!is_location_shared){
11        this.pusher.unsubscribe(`private-friend-${user_id}`); // disconnect from their own channel
12        if(this.watchId){
13          navigator.geolocation.clearWatch(this.watchId);
14        }
15      }else{
16        this.user_channel = this.pusher.subscribe(`private-friend-${user_id}`);
17        this.user_channel.bind('client-friend-subscribed', (friend_data) => {
18
19          let friends_count = this.state.subscribed_friends_count + 1;
20          this.setState({
21            subscribed_friends_count: friends_count
22          });
23
24          if(friends_count == 1){ // only begin monitoring the location when the first subscriber subscribes
25            this.watchId = navigator.geolocation.watchPosition(
26              (position) => {
27                var region = regionFrom(
28                  position.coords.latitude,
29                  position.coords.longitude,
30                  position.coords.accuracy
31                );
32                this.user_channel.trigger('client-location-changed', region); // push the data to subscribers
33              }
34            );
35          }
36        });  
37
38      }
39    }

The onViewLocation() function is executed when the user taps on any friend on their friend list. This is where we subscribe to the friend’s channel so we can get updates whenever their location changes:

1onViewLocation(friend) {
2
3      this.friend_channel = this.pusher.subscribe(`private-friend-${friend.id}`);
4      this.friend_channel.bind('pusher:subscription_succeeded', () => {
5        let username = this.state.user.name;
6        this.friend_channel.trigger('client-friend-subscribed', {
7          name: username
8        });
9      });
10
11      this.setState({
12        subscribed_to: friend.id
13      });
14
15      // add code for navigating to the map page
16    }

Next, add the code for navigating to the map page. Pass in the name of the friend and the reference to the friend’s channel as navigation props. This allows those values to be accessed from the map page later on:

1const { navigate } = this.props.navigation;
2
3    navigate('MapPage', {
4      name: friend.name,
5      friend_channel: this.friend_channel // pass the reference to the friend's channel
6    });

Render the index page. This consists of the user’s profile, their friendslist, and the Facebook login or logout button:

1render() {
2
3      return (
4        <View style={styles.page_container}>
5        {
6          this.state.is_loggedin &&
7          <View style={styles.container}>
8          {
9            this.state.user &&
10            <View style={styles.profile_container}>
11              <Profile
12                profile_picture={this.state.user.photo}
13                profile_name={this.state.user.name}
14              />
15
16              <Text>Share Location</Text>
17              <Switch
18                value={this.state.is_location_shared}
19                onValueChange={this.toggleLocationSharing} />
20            </View>
21          }
22
23          {
24            this.state.friends &&
25            <Friends
26              friends={this.state.friends}
27              onViewLocation={this.onViewLocation} />
28          }
29          </View>
30        }
31
32          <FBLogin
33            permissions={["email", "user_friends"]}
34            onLogin={this.onLogin}
35            onLoginFound={this.onLoginFound}
36            onLogout={this.onLogout}
37            style={styles.button}
38          />
39        </View>
40      );
41
42    }
43
44    // add the styles
45    const styles = StyleSheet.create({
46      page_container: {
47        ...StyleSheet.absoluteFillObject,
48        justifyContent: 'flex-end'
49      },
50      container: {
51        flex: 1,
52        padding: 20
53      },
54      profile_container: {
55        flex: 1,
56        alignItems: 'center',
57        marginBottom: 50
58      },
59      button: {
60        paddingBottom: 30,
61        marginBottom: 20,
62        alignSelf: 'center'
63      }
64    });

The code above is pretty self-explanatory so I won’t go into details about what each line does.

Profile Component

The Profile component is used for displaying the user’s profile picture and name:

1import React, { Component } from 'react';
2    import {
3      StyleSheet,
4      Text,
5      View,
6      Image
7    } from 'react-native';
8
9    import PropTypes from 'prop-types';
10
11    class Profile extends Component<{}> {
12
13      render() {
14
15        return (
16          <View style={styles.profile_container}>
17            <Image
18              resizeMode={"contain"}
19              source={{uri: this.props.profile_picture}}
20              style={styles.profile_photo}
21            />
22            <Text style={styles.profile_name}>{this.props.profile_name}</Text>
23          </View>
24        );
25
26      }
27
28    }
29
30    const styles = StyleSheet.create({
31      profile_container: {
32        alignItems: 'center'
33      },
34      profile_photo: {
35        height: 100,
36        width: 100
37      },
38      profile_name: {
39        fontWeight: 'bold',
40        fontSize: 18
41      }
42    });
43
44    // specify the required props
45    Profile.propTypes = {
46      profile_picture: PropTypes.string.isRequired,
47      profile_name: PropTypes.string.isRequired
48    };
49
50    export default Profile;

Friends Component

The Friends component is used for rendering the list of friends:

1import React, { Component } from 'react';
2    import {
3      StyleSheet,
4      Text,
5      View,
6      Image,
7      TouchableHighlight
8    } from 'react-native';
9
10    import PropTypes from 'prop-types';
11
12    class Friends extends Component<{}> {
13
14      renderFriends() {
15        return this.props.friends.map((friend, index) => {
16
17          let profile_picture = `https://graph.facebook.com/${friend.id}/picture?width=50`;
18          return (
19            <TouchableHighlight
20              key={index}
21              onPress={this.props.onViewLocation.bind(this, friend)}
22              underlayColor={"#CCC"}>
23
24                <View style={styles.friend_row}>
25                  <Image
26                    resizeMode={"contain"}
27                    source={{uri: profile_picture}}
28                    style={styles.profile_photo}
29                  />
30                  <Text style={styles.friend_name}>{friend.name}</Text>
31                </View>
32
33            </TouchableHighlight>
34          );
35        });
36      }
37
38      render() {
39
40        return (
41          <View style={styles.friends_container}>
42            <Text style={styles.friends_header_text}>View Friend Location</Text>
43            {this.renderFriends.call(this)}
44          </View>
45        );
46
47      }
48    }
49
50    // add the styles
51    const styles = StyleSheet.create({
52      friends_container: {
53        flex: 2
54      },
55      friends_header_text: {
56        fontSize: 18,
57        fontWeight: 'bold'
58      },
59      friend_row: {
60        flexDirection: 'row',
61        alignItems: 'center',
62        padding: 10
63      },
64      profile_photo: {
65        width: 50,
66        height: 50,
67        marginRight: 20
68      },
69      friend_name: {
70        fontSize: 15
71      }
72    });
73
74    // specify the required props
75    Friends.propTypes = {
76      friends: PropTypes.arrayOf(
77        PropTypes.shape({
78          id: PropTypes.string.isRequired,
79          name: PropTypes.string.isRequired
80        })
81      ),
82      onViewLocation: PropTypes.func.isRequired
83    };
84
85    export default Friends;

Region Helper

Earlier, we’ve used a function called regionFrom but we haven’t really created it yet. So go ahead and create a src/helpers.js file and add the following:

1export function regionFrom(lat, lon, accuracy) {
2      const oneDegreeOfLongitudeInMeters = 111.32 * 1000;
3      const circumference = (40075 / 360) * 1000;
4
5      const latDelta = accuracy * (1 / (Math.cos(lat) * circumference));
6      const lonDelta = (accuracy / oneDegreeOfLongitudeInMeters);
7
8      return {
9        latitude: lat,
10        longitude: lon,
11        latitudeDelta: Math.max(0, latDelta),
12        longitudeDelta: Math.max(0, lonDelta)
13      };
14    }

This function is used for getting the latitude and longitude delta values needed by the React Native Maps library to display a map.

Map Page

Now we move over to the map page. Create a src/map_page.js file and add the following:

1import React, { Component } from 'react';
2    import {
3      StyleSheet,
4      Text,
5      View,
6      DeviceEventEmitter
7    } from 'react-native';
8
9    import Map from './map';
10
11    import { regionFrom } from '../helpers';
12
13    // add code for creating the component

Create the component, and set the page title based on the parameters passed from the index page:

1export default class MapPage extends Component<{}> {
2
3      static navigationOptions = ({navigation}) => ({
4        title: `${navigation.state.params.name}'s Location`,
5      });
6
7     // add constructor code 
8    }

Set a default location in the constructor so that a map is still displayed even if the user is not sharing their location:

1constructor() {
2      super();
3
4      // set default location
5      let region = {
6        "latitude": 35.4625901,
7        "longitude": 138.65437569999995,
8        "latitudeDelta": 0,
9        "longitudeDelta": 0
10      };
11
12      this.state = {
13        region
14      }
15    }

When the user taps on the back button, componentWillUnmount() is triggered as the component goes out of view. So this is the perfect time to trigger the unsubscribe event to let the index page know that the user has stopped viewing their friend’s location.

1componentWillUnmount() {
2      DeviceEventEmitter.emit('unsubscribe',  {
3        unsubscribe: true
4      });
5    }

When the component is mounted, we want to start listening for when the location changes so we can update the map accordingly:

1componentDidMount() {
2
3      const { state } = this.props.navigation;
4      state.params.friend_channel.bind('client-location-changed', (data) => {
5        this.setState({
6          region: data
7        });
8      });
9
10    }

The render() method simply outputs the Map component:

1render() {
2
3      return (
4        <View style={styles.map_container}>
5        {
6          this.state.region &&
7          <Map region={this.state.region} />
8        }
9        </View>
10      );
11
12    }

Add the styles:

1const styles = StyleSheet.create({
2      map_container: {
3        ...StyleSheet.absoluteFillObject,
4        justifyContent: 'flex-end'
5      }
6    });

Map Component

Lastly, there’s the Map component which is used to actually render the Google Map. This uses the React Native Maps package that we installed earlier. There are only two components that you need in order to make it work: MapView and MapView.Marker. MapView is used to render the map, and MapView.Marker is used to rendering the marker:

1import React, { Component } from 'react';
2    import {
3      StyleSheet,
4      View
5    } from 'react-native';
6
7    import MapView from 'react-native-maps';
8    import PropTypes from 'prop-types';
9
10    class Map extends Component<{}> {
11
12      render() {
13
14        return (
15          <View style={styles.map_container}>
16            {
17            this.props.region &&
18              <MapView
19                style={styles.map}
20                region={this.props.region}
21              >
22                <MapView.Marker
23                  coordinate={{
24                    latitude: this.props.region.latitude,
25                    longitude: this.props.region.longitude}}
26                />
27              </MapView>
28            }
29          </View>
30        );
31
32      }
33
34    }
35
36    // add the styles
37    const styles = StyleSheet.create({
38      map_container: {
39        ...StyleSheet.absoluteFillObject,
40        justifyContent: 'flex-end'
41      },
42      map: {
43        ...StyleSheet.absoluteFillObject,
44      },
45    });
46
47    // specify the required props
48    Map.propTypes = {
49      region: PropTypes.shape({
50        latitude: PropTypes.number.isRequired,
51        longitude: PropTypes.number.isRequired,
52        latitudeDelta: PropTypes.number.isRequired,
53        longitudeDelta: PropTypes.number.isRequired
54      })
55    };
56
57    export default Map;

Running the App

You only need one device and one emulator in order to test the app. First, run the app on your device by executing react-native run-android. Once the app is running, disconnect the device and open a Genymotion virtual device. Execute the same command again to run the app on the virtual device. Don’t forget to add another Facebook user, aside from your own Facebook account as a tester or developer under the Facebook app settings. You can do that by clicking on the Roles tab and searching for the user in there. Only Facebook users that are added in the app settings can log in. This is because the Facebook app is still unpublished.

Genymotion has a built-in functionality for spoofing the GPS coordinates. This will trigger the geolocation functionality in the app every time the location changes (either by pointing the marker on a different location on the map or searching for another place). That’s why it’s best to use Genymotion for testing the user who is broadcasting their location. If you’re going to follow this route, be sure to check out the documentation on how to install Google Services on Genymotion since the Google Map functionality uses Google Services.

If you don’t have any device to test on, you can use Genymotion and the Pusher debug console to test the app. All you have to do is figure out the Facebook user ID of the two users you’re using for testing. You can do that by using this tool. Login with your Facebook account on Genymotion then click on one of the other accounts. You can then emulate the location update by manually entering the coordinates on the debug console. You can access the debug console from your Pusher app’s dashboard:

You can use the following as initial values:

  • channel name: private-friend-YOUR-ACCOUNTS-FB-ID
  • event: client-location-changed
  • data: you can use the following data for testing:
1{
2      "latitude": 16.6105538,
3      "longitude": 120.31429539999999,
4      "latitudeDelta": 0,
5      "longitudeDelta": 0
6    }

Send the event once that’s done. Sending the event should update the map on the app. You can use a service such as latlong.net to come up with the coordinates of different places.

Suggestions for Improvement

If you want to improve the app, here are some ideas on what you can add:
– The number of friends that are currently viewing the user’s location doesn’t actually get updated when someone disconnects from the user’s channel. You can add a listener for when someone disconnects so that you can update the value as well.
– The current user doesn’t actually know who are the people that are currently subscribed to their location. For this, you can use an alert dialog everytime someone subscribes to the channel. The client-friend-subscribed event has already been laid out for this purpose. You can even take the idea further by making use of Presence channels. This comes with an additional feature that allows you to keep track of the people that are subscribed to a specific channel.
– Add notifications to inform the subscribed users for when the user they’re subscribed to disables location sharing.

Conclusion

That’s it! In this tutorial you’ve learned how to create a location-sharing app which uses React Native’s built-in Geolocation library and Pusher to broadcast the data to the user’s friends. You can check out the project’s complete source code in its Github repo.