/ Tutorials

React Native Maps Tutorial: Find My Friends

In this tutorial, we're going to be building a "Find My Friends"-like app using React Native, react-native-maps and the Scaledrone React Native client.

You can find the full source code from GitHub.

This tutorial will teach you:

  • How to draw a map using react-native-maps and an overlay UI on top of it.
  • How to track your current location and send it to your friends.
  • How to move markers in real-time using Scaledrone.

This tutorial expects a basic understanding of React Native.

Project structure

The project is divided into two independent parts:

  • A React Native app. You can run the app in an iOS emulator. If you wish to run the code on Android scroll to the very bottom.
  • An authentication server. The server authenticates a joining user and controls which parts of Scaledrone they can access.

This is how our app will work:

swimlanes-diagram

Creating the React Native app

Setting up react-native-maps

You can find the freshest info on how to set up react-native-maps from their setup guide.

All of the code in this tutorial be run using Apple Maps. Apple Maps is the default maps provider on iOS. That way you don't need to configure Google Maps.

Rendering the map

Let's get started with the App.js file. As we progress, we'll be adding more code to this file.

If you ever get stuck following this tutorial check out the full source file on GitHub.

The following code will draw a full-screen map and a button.

import React, {Component} from 'react';
import {
  StyleSheet,
  View,
  Text,
  Dimensions,
  TouchableOpacity,
  AlertIOS,
} from 'react-native';
import MapView, {Marker, AnimatedRegion} from 'react-native-maps';

const screen = Dimensions.get('window');

const ASPECT_RATIO = screen.width / screen.height;
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;

export default class App extends Component {

  render() {
    return (
      <View style={styles.container}>
        <MapView
          style={styles.map}
          ref={ref => {this.map = ref;}}
          initialRegion={{
            latitude: 37.600425,
            longitude: -122.385861,
            latitudeDelta: LATITUDE_DELTA,
            longitudeDelta: LONGITUDE_DELTA,
          }}
        >
          {this.createMarkers()}
        </MapView>
        <View pointerEvents="none" style={styles.members}>
          {this.createMembers()}
        </View>
        <View style={styles.buttonContainer}>
          <TouchableOpacity
            onPress={() => this.fitToMarkersToMap()}
            style={[styles.bubble, styles.button]}
          >
            <Text>Fit Markers Onto Map</Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'flex-end',
    alignItems: 'center',
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
  bubble: {
    flex: 1,
    backgroundColor: 'rgba(255,255,255,0.7)',
    paddingHorizontal: 18,
    paddingVertical: 12,
    borderRadius: 20,
    marginRight: 20,
  },
  button: {
    width: 80,
    paddingHorizontal: 12,
    alignItems: 'center',
    marginHorizontal: 10,
  },
  buttonContainer: {
    flexDirection: 'row',
    marginVertical: 20,
    backgroundColor: 'transparent',
    marginBottom: 400,
  },
  members: {
    flexDirection: 'column',
    justifyContent: 'flex-start',
    alignItems: 'flex-start',
    width: '100%',
    paddingHorizontal: 10,
  },
});

Connecting to Scaledrone

Now that we have a map set up we can start adding additional logic to it. Next, we should connect to Scaledrone. We'll be using Scaledrone to keep track of the connected users and receive and publish real-time location updates.

Don't worry, Scaledrone is free for up to 20 concurrent users and 100,000 daily events. Just go to scaledrone.com, sign up and create a new Scaledrone channel.

Within the Scaledrone channel settings, enable the message history and set the authentication setting to Always require authentication.

Scaledrone dashboard channel

Let's add the constructor() and componentDidMount() functions. Inside the constructor() we're going to initialise a state holding an array of connected members. The member object is the Scaledrone member object to which we will also assign an AnimatedRegion marker location.

Example member object:

{
  id: '6jmItzqXbT:rT9M2uNHxK', // unique user ID assigned by Scaledrone
  authData: { // authData is created by the JWT sent from the authentication server
    color: '#f032e6', // unique color hash from the authentication server
    name: 'John' // user is prompted to insert their name
  },
  location: AnimatedRegion // react-native-maps marker location
}

Now that you know what the member object looks like let's take a look at the explanation for the code block in which we connect to Scaledrone.

  1. We create a new instance of Scaledrone to which you have to pass the ID of the newly created channel.
  2. We use AlertIOS.prompt() to ask for the user's name.
  3. We do a request to the authentication server (which we will create in the second part of the tutorial) using doAuthRequest().
  4. We subscribe to the 'observable-locations' room. You can name the room anything you want, but for the observable rooms feature to work, the room needs to be prefixed with 'observable-'.
  5. We set up listeners for a few events:
    • The open event shows that the user connected to the room. Once we receive this, we can start tracking our location. We'll define this function at a later step.
    • The data event listens to new incoming messages (user locations in our case).
    • The history_message event provides the past messages. We ask for 50 messages from the room's history.
    • The members event lists all of the users connected to the room at the time of the connection.
    • The member_join event shows that a new user joined the room.
    • The member_leave event shows that a user has left the room.
const Scaledrone = require('scaledrone-react-native');
.
.
.

export default class App extends Component {

  constructor() {
    super();
    this.state = {
      members: []
    };
  }

  componentDidMount() {
    const drone = new Scaledrone('<YOUR_SCALEDRONE_CHANNEL_ID>');
    drone.on('error', error => console.error(error));
    drone.on('close', reason => console.error(reason));
    drone.on('open', error => {
      if (error) {
        return console.error(error);
      }
      AlertIOS.prompt(
        'Please insert your name',
        null,
        name => doAuthRequest(drone.clientId, name).then(
          jwt => drone.authenticate(jwt)
        )
      );
    });
    const room = drone.subscribe('observable-locations', {
      historyCount: 50 // load 50 past messages
    });
    room.on('open', error => {
      if (error) {
        return console.error(error);
      }
      this.startLocationTracking(position => {
        const {latitude, longitude} = position.coords;
        // publish device's new location
        drone.publish({
          room: 'observable-locations',
          message: {latitude, longitude}
        });
      });
    });
    // received past message
    room.on('history_message', message =>
      this.updateLocation(message.data, message.clientId)
    );
    // received new message
    room.on('data', (data, member) =>
      this.updateLocation(data, member.id)
    );
    // array of all connected members
    room.on('members', members =>
      this.setState({members})
    );
    // new member joined room
    room.on('member_join', member => {
      const members = this.state.members.slice(0);
      members.push(member);
      this.setState({members});
    });
    // member left room
    room.on('member_leave', member => {
      const members = this.state.members.slice(0);
      const index = members.findIndex(m => m.id === member.id);
      if (index !== -1) {
        members.splice(index, 1);
        this.setState({members});
      }
    });
  }
  
  .
  .
  .

Install Scaledrone:

npm install scaledrone-react-native --save

Tracking our location

Tracking the location of your device is pretty straightforward. React Native supports the HTML5 Geolocation API. Each time we receive a location update we publish it to the Scaledrone room (we did that in the componentDidMount() function).

If you're running the tutorial on an iOS emulator, you might wonder why your pin ends up in San Francisco. This is the default location of the iOS emulator's Geolocation API.

startLocationTracking(callback) {
  navigator.geolocation.watchPosition(
    callback,
    error => console.error(error),
    {
      enableHighAccuracy: true,
      timeout: 20000,
      maximumAge: 1000
    }
  );
}

The location message we send to the other users looks like this:

{
    longitude: -122.164275,
    latitude: 37.442909
}

Rendering the online users list

The createMembers() method will display a list of currently online users on the bottom left of the map. Each user will be prompted to enter their name and will also be assigned a unique color from our authentication server (which we will be creating later).

Find My Friends Online Users

We're iterating over all of the members in our local state and rendering them as a colorful circle and a name.

createMembers() {
  const {members} = this.state;
  return members.map(member => {
    const {name, color} = member.authData;
    return (
      <View key={member.id} style={styles.member}>
        <View style={[styles.avatar, {backgroundColor: color}]}/>
        <Text style={styles.memberName}>{name}</Text>
      </View>
    );
  });
}

We'll also be adding style to the elements:

member: {
  flexDirection: 'row',
  justifyContent: 'center',
  alignItems: 'center',
  backgroundColor: 'rgba(255,255,255,1)',
  borderRadius: 20,
  height: 30,
  marginTop: 10,
},
memberName: {
  marginHorizontal: 10,
},
avatar: {
  height: 30,
  width: 30,
  borderRadius: 15,
}

Rendering the map markers

Find My Friends Markers

Each of the connected members will be displayed as an animated Marker on the map. The marker (or a pin in Apple Maps) will be colored, assigned a title and set to a coordinate on the map.

We're excluding members who don't have a location assigned to them. This situation can occur when a member just joined the Scaledrone room but hasn't yet published its location.

createMarkers() {
  const {members} = this.state;
  const membersWithLocations = members.filter(m => !!m.location);
  return membersWithLocations.map(member => {
    const {id, location, authData} = member;
    const {name, color} = authData;
    return (
      <Marker.Animated
        key={id}
        identifier={id}
        coordinate={location}
        pinColor={color}
        title={name}
      />
    );
  });
}

Fitting markers onto the screen

To make it easier to find the markers we've added the "Fit Markers Onto Map" button. The react-native-maps fitToSuppliedMarkers() function works by taking an array of Marker IDs and zooming the view so that all the provided markers are visible on the screen.

fitToMarkersToMap() {
  const {members} = this.state;
  this.map.fitToSuppliedMarkers(members.map(m => m.id), true);
}

Authenticating the connection

To authenticate the connection we're going to be using Scaledrone's JSON Web Token authentication. JWT authentication is handy because it allows for your own servers to define precise access levels - the users will only be able to access the rooms you allow them to.

In the next section of the tutorial, we're going to be building a small Node.js authentication server running on port 3000.

But first, let's make a POST request to that server on the URL http://localhost:3000/auth and send it the user's clientId (accessible via drone.clientId) and the name.

If we're allowed to connect the server will provide us with a JSON Web Token defining our access rights inside the Scaledrone channel. We will then pass that token on to Scaledrone.

The open handler triggers this request, and the actual request looks like this:

function doAuthRequest(clientId, name) {
  let status;
  return fetch('http://localhost:3000/auth', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({clientId, name}),
  }).then(res => {
    status = res.status;
    return res.text();
  }).then(text => {
    if (status === 200) {
      return text;
    } else {
      alert(text);
    }
  }).catch(error => console.error(error));
}

Authentication Server

In this part of the tutorial, we'll be looking at the JWT authentication server.

The authentication server is a simple Express app which contains a single POST /auth route.

After the user receives the open event from Scaledrone the React Native app does a request to the server passing the name and clientId as POST parameters. If the user provides a valid name and clientId, we generate a JSON Web Token using the jsonwebtoken library.

The JWT defines which rooms the client can access using the permissions clause. The room name is described as a regex. Inside the room, the user will be allowed to subscribe to messages, publish messages and query up to 50 messages from the history.

const express = require('express');
const jwt = require('jsonwebtoken');
const colors = require('./colors');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();
app.use(bodyParser.json());
app.use(cors());

app.post('/auth', (req, res) => {
  const {clientId, name} = req.body;
  if (!clientId || clientId.length < 1) {
    res.status(400).send('Invalid ID');
  }
  if (!name || name.length < 1) {
    res.status(400).send('Invalid name');
  }
  const token = jwt.sign({
    client: clientId,
    channel: '<YOUR_SCALEDRONE_CHANNEL_ID>',
    permissions: {
      "^observable-locations$": {
        publish: true,
        subscribe: true,
        history: 50,
      }
    },
    data: {
      name,
      color: colors.get()
    },
    exp: Math.floor(Date.now() / 1000) + 60 * 3 // expire in 3 minutes
  }, '<YOUR_SCALEDRONE_CHANNEL_SECRET>');
  res.send(token);
});

app.listen(3000, () => console.log('Server listening on port 3000'));

Putting it together

To test your newly created app start by running the authentication server:

cd server-dir
node index.js

Then run the iOS emulator:

react-native run-ios

Once everything has been set up correctly, you should see a single marker on the map. Even if you're testing on an actual device, you are likely sitting behind your computer and your marker not moving. That's no fun, and it makes testing a real pain. That's why we created the random_movement.js file to GitHub. Run it as a Node.js script and you will see three randomly moving users on the map:

node random_movement.js

Conclusion

As you can see, we created a simple Find My Friends clone with only a couple hundred lines of code. You can find the full source code or run the working prototype on GitHub. If you have any questions or feedback feel free to contact us.

Running on Android

With minimal code changes, this tutorial should run easily on Android. The required changes are:

Send realtime data to your users
We take care of complex realtime infrastructure problems so you can focus on what you enjoy - building awesome apps
Build realtime features now