In this tutorial, we’ll create a ride hailing app(similar to Uber or Lyft) with React Native and Pusher. React Native will be used to create an Android app for both the driver and the passenger. Pusher will be used for realtime communication between the two.
The clone that we’re going to create will pretty much have the same flow as any ride hailing app out there: passenger books a ride → app looks for a driver → driver accepts the request → driver picks up passenger → driver drives to destination → passenger pays the driver.
From the passenger app, the user clicks on “Book a Ride”.
A modal will open that will allow the passenger to pick the place where they want to go.
At this point, the ride ends and the passenger can book another ride. The driver is also free to accept any incoming ride request.
Next, click on the “App Settings” tab and check “Enable client events”. This allows us to have the driver and passenger app directly communicate with each other.
Last, click on the “App keys” and copy the credentials. If you’re worried about pricing, the Pusher sandbox plan is pretty generous so you can use it for free when testing the app.
While you’re there, click on the “SDK Tools” and make sure that you also have the same tools installed as mine:
First, let’s work on the auth server. This is required because we will be sending client events from the app, client events requires the Pusher channel to be private, and private channels have restricted access. This is where the auth server comes in. It serves as a way for Pusher to know if a user that’s trying to connect is indeed a registered user of the app.
Start by installing the dependencies:
npm install --save express body-parser pusher
Next, create a server.js
file and add the following code:
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 })); 8 9 var pusher = new Pusher({ // connect to pusher 10 appId: process.env.APP_ID, 11 key: process.env.APP_KEY, 12 secret: process.env.APP_SECRET, 13 cluster: process.env.APP_CLUSTER, 14 }); 15 16 app.get('/', function(req, res){ // for testing if the server is running 17 res.send('all is well...'); 18 }); 19 20 // for authenticating users 21 app.get("/pusher/auth", function(req, res) { 22 var query = req.query; 23 var socketId = query.socket_id; 24 var channel = query.channel_name; 25 var callback = query.callback; 26 27 var auth = JSON.stringify(pusher.authenticate(socketId, channel)); 28 var cb = callback.replace(/\"/g,"") + "(" + auth + ");"; 29 30 res.set({ 31 "Content-Type": "application/javascript" 32 }); 33 34 res.send(cb); 35 }); 36 37 app.post('/pusher/auth', function(req, res) { 38 var socketId = req.body.socket_id; 39 var channel = req.body.channel_name; 40 var auth = pusher.authenticate(socketId, channel); 41 res.send(auth); 42 }); 43 44 var port = process.env.PORT || 5000; 45 app.listen(port);
I’m no longer going to go into detail what the code above does since its already explained in the docs for Authenticating Users.
To keep things simple, I haven’t added the code to check if a user really exists in a database. You can do that in the /pusher/auth
endpoint by checking if a username exists. Here’s an example:
1var users = ['luz', 'vi', 'minda']; 2 var username = req.body.username; 3 4 if(users.indexOf(username) !== -1){ 5 var socketId = req.body.socket_id; 6 var channel = req.body.channel_name; 7 var auth = pusher.authenticate(socketId, channel); 8 res.send(auth); 9 } 10 11 // otherwise: return error
Don’t forget to pass in the username
when connecting to Pusher on the client-side later on.
Try running the server once that’s done:
node server.js
Access http://localhost:5000
on your browser to see if it works.
Since Pusher will have to connect to the auth server, it needs to be accessible from the internet. You can use now.sh to deploy the auth server. You can install it with the following command:
npm install now
Once installed, you can now navigate to the folder where you have the server.js
file and execute now
. You’ll be asked to enter your email and verify your account.
Once your account is verified, execute the following to add your Pusher app settings as environment variables to your now.sh account so you can use it from inside the server:
1now secret add pusher_app_id YOUR_PUSHER_APP_ID 2 now secret add pusher_app_key YOUR_PUSHER_APP_KEY 3 now secret add pusher_app_secret YOUR_PUSHER_APP_SECRET 4 now secret add pusher_app_cluster YOUR_PUSHER_APP_CLUSTER
Next, deploy the server while supplying the secret values that you’ve added:
now -e APP_ID=@pusher_app_id -e APP_KEY=@pusher_app_key -e APP_SECRET=@pusher_app_secret APP_CLUSTER=@pusher_app_cluster
This allows you to access your Pusher app settings from inside the server like so:
process.env.APP_ID
The deploy URL that now.sh returns is the URL that you’ll use later on to connect the app to the auth server.
First, create a new React Native app:
react-native init grabDriver
Once that’s done, navigate inside the grabDriver
directory and install the libraries that we’ll need. This includes pusher-js for working with Pusher, React Native Maps for displaying a map, and React Native Geocoding for reverse-geocoding coordinates to the actual name of a place:
npm install --save pusher-js react-native-maps react-native-geocoding
Once all the libraries are installed, React Native Maps needs some additional steps in order for it to work. First is linking the project resources:
react-native link react-native-maps
Next, you need to create a Google project, get an API key from the Google developer console, and enable the Google Maps Android API and Google Maps Geocoding API. After that, open the android\app\src\main\AndroidManifest.xml
file in your project directory. Under the <application>
tag, add a <meta-data>
containing the server API key.
1<application> 2 <meta-data 3 android:name="com.google.android.geo.API_KEY" 4 android:value="YOUR GOOGLE SERVER API KEY"/> 5 </application>
While you’re there, add the following below the default permissions. This allows us to check for network status and request for Geolocation data from the device.
1<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 2 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Also make sure that it's targeting the same API version as the device you installed with Genymotion. As I’ve said earlier, if its version 23 or above you won’t really need to do anything, but if its lower than that then it has to be exact for the app to work.
1<uses-sdk 2 android:minSdkVersion="16" 3 android:targetSdkVersion="23" />
Lastly, since we’ll be primarily using Genymotion for testing the driver app, you need to follow the instructions here.
We need to do this because the React Native Maps library primarily uses Google Maps. We need to add Google Play Services in order for it to work. Unlike most Android phones which already comes with this installed, Genymotion doesn’t have it by default due to intellectual property reasons. Thus, we need to manually install it.
If you’re reading this sometime after it was published, be sure to check out the Installation docs to make sure you’re not missing anything.
Now you’re ready to start coding the app. Start by opening the index.android.js
file and replace the default code with the following:
1import { AppRegistry } from 'react-native'; 2 import App from './App'; 3 AppRegistry.registerComponent('grabDriver', () => App);
What this does is importing the App
component which is the main component for the app. It is then registered as the default component so it will be rendered on the screen.
Next, create the App.js
file and import the things we need from the React Native package:
1import React, { Component } from 'react'; 2 import { 3 StyleSheet, 4 Text, 5 View, 6 Alert 7 } from 'react-native';
Also, import the third-party libraries that we installed earlier:
1import Pusher from 'pusher-js/react-native'; 2 import MapView from 'react-native-maps'; 3 4 import Geocoder from 'react-native-geocoding'; 5 Geocoder.setApiKey('YOUR GOOGLE SERVER API KEY');
Lastly, import the helpers
file:
import { regionFrom, getLatLonDiffInMeters } from './helpers';
The helpers.js
file contains 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 } 15 16 export function getLatLonDiffInMeters(lat1, lon1, lat2, lon2) { 17 var R = 6371; // Radius of the earth in km 18 var dLat = deg2rad(lat2-lat1); // deg2rad below 19 var dLon = deg2rad(lon2-lon1); 20 var a = 21 Math.sin(dLat/2) * Math.sin(dLat/2) + 22 Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * 23 Math.sin(dLon/2) * Math.sin(dLon/2) 24 ; 25 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 26 var d = R * c; // Distance in km 27 return d * 1000; 28 } 29 30 function deg2rad(deg) { 31 return deg * (Math.PI/180) 32 }
These functions are used for getting the latitude and longitude delta values needed by the React Native Maps library to display a map. The other function (getLatLonDiffInMeters
) is used for determining the distance in meters between two coordinates. Later on, this will allow us to inform the user’s whether they’re already near each other or when they’re near their destination.
Next, create the main app component and declare the default states:
1export default class grabDriver extends Component { 2 3 state = { 4 passenger: null, // for storing the passenger info 5 region: null, // for storing the current location of the driver 6 accuracy: null, // for storing the accuracy of the location 7 nearby_alert: false, // whether the nearby alert has already been issued 8 has_passenger: false, // whether the driver has a passenger (once they agree to a request, this becomes true) 9 has_ridden: false // whether the passenger has already ridden the vehicle 10 } 11 } 12 // next: add constructor code
Inside the constructor, initialize the variables that will be used throughout the app:
1constructor() { 2 super(); 3 4 this.available_drivers_channel = null; // this is where passengers will send a request to any available driver 5 this.ride_channel = null; // the channel used for communicating the current location 6 // for a specific ride. Channel name is the username of the passenger 7 8 this.pusher = null; // the pusher client 9 } 10 11 // next: add code for connecting to pusher
Before the component is mounted, connect to the auth server that you created earlier. Be sure to replace the values for the pusher key, authEndpoint
and cluster
.
1componentWillMount() { 2 this.pusher = new Pusher('YOUR PUSHER KEY', { 3 authEndpoint: 'YOUR PUSHER AUTH SERVER ENDPOINT', 4 cluster: 'YOUR PUSHER CLUSTER', 5 encrypted: true 6 }); 7 8 // next: add code for listening to passenger requests 9 }
Now that you’ve connected to the auth server, you can now start listening for requests coming from the passenger app. The first step is to subscribe to a private channel. This channel is where all passengers and drivers subscribe to. In this case, its used by drivers to listen for ride requests. It needs to be a private channel because client events can only be triggered on private and presence channels due to security reasons. You know that it’s a private channel because of the private-
prefix.
this.available_drivers_channel = this.pusher.subscribe('private-available-drivers'); // subscribe to "available-drivers" channel
Next, listen to the client-driver-request
event. You know that this is a client event because of the client-
prefix. Client events don't need server intervention in order to work, the messages are sent directly to from client to client. That’s the reason why we need an auth server to make sure all the users that are trying to connect are real users of the app.
Going back to the code, we listen for client events by calling the bind
method on the channel that we subscribed to and passing in the name of the event as the first argument. The second argument is the function that you want to execute once this event is triggered from another client (from anyone using the passenger app to request a ride). In the code below, we show an alert message asking the driver if they want to accept the passenger. Note that the app assumes that there can only be one passenger at any single time.
1// listen to the "driver-request" event 2 this.available_drivers_channel.bind('client-driver-request', (passenger_data) => { 3 4 if(!this.state.has_passenger){ // if the driver has currently no passenger 5 // alert the driver that they have a request 6 Alert.alert( 7 "You got a passenger!", // alert title 8 "Pickup: " + passenger_data.pickup.name + "\nDrop off: " + passenger_data.dropoff.name, // alert body 9 [ 10 { 11 text: "Later bro", // text for rejecting the request 12 onPress: () => { 13 console.log('Cancel Pressed'); 14 }, 15 style: 'cancel' 16 }, 17 { 18 text: 'Gotcha!', // text for accepting the request 19 onPress: () => { 20 // next: add code for when driver accepts the request 21 } 22 }, 23 ], 24 { cancelable: false } // no cancel button 25 ); 26 27 } 28 29 });
Once the driver agrees to pick up the passenger, we subscribe to their private channel. This channel is reserved only for communication between the driver and the passenger, that’s why we’re using the unique passenger username as part of the channel’s name.
this.ride_channel = this.pusher.subscribe('private-ride-' + passenger_data.username);
Not unlike the available-drivers
channel, we’ll need to listen for when the subscription actually succeeded (pusher:subscription_succeeded
) before we do anything else. This is because we’re going to immediately trigger a client event to be sent to the passenger. This event (client-driver-response
) is a handshake event to let the passenger know that the driver they sent their request to is still available. If the passenger still hasn’t gotten a ride at that time, the passenger app triggers the same event to let the driver know that they’re still available for picking up. At this point, we update the state so that the UI changes accordingly.
1this.ride_channel.bind('pusher:subscription_succeeded', () => { 2 // send a handshake event to the passenger 3 this.ride_channel.trigger('client-driver-response', { 4 response: 'yes' // yes, I'm available 5 }); 6 7 // listen for the acknowledgement from the passenger 8 this.ride_channel.bind('client-driver-response', (driver_response) => { 9 10 if(driver_response.response == 'yes'){ // passenger says yes 11 12 //passenger has no ride yet 13 this.setState({ 14 has_passenger: true, 15 passenger: { 16 username: passenger_data.username, 17 pickup: passenger_data.pickup, 18 dropoff: passenger_data.dropoff 19 } 20 }); 21 22 // next: reverse-geocode the driver location to the actual name of the place 23 24 }else{ 25 // alert that passenger already has a ride 26 Alert.alert( 27 "Too late bro!", 28 "Another driver beat you to it.", 29 [ 30 { 31 text: 'Ok' 32 }, 33 ], 34 { cancelable: false } 35 ); 36 } 37 38 }); 39 40 });
Next, we use the Geocoding library to determine the name of the place where the driver is currently at. Behind the scenes, this uses the Google Geocoding API and it usually returns the street name. Once we get a response back, we trigger the found-driver
event to let the passenger know that the app has found a driver for them. This contains driver info such as the name and the current location.
1Geocoder.getFromLatLng(this.state.region.latitude, this.state.region.longitude).then( 2 (json) => { 3 var address_component = json.results[0].address_components[0]; 4 5 // inform passenger that it has found a driver 6 this.ride_channel.trigger('client-found-driver', { 7 driver: { 8 name: 'John Smith' 9 }, 10 location: { 11 name: address_component.long_name, 12 latitude: this.state.region.latitude, 13 longitude: this.state.region.longitude, 14 accuracy: this.state.accuracy 15 } 16 }); 17 18 }, 19 (error) => { 20 console.log('err geocoding: ', error); 21 } 22 ); 23 // next: add componentDidMount code
Once the component is mounted, we use React Native’s Geolocation API to watch for location updates. The function that you pass to the watchPosition
function gets executed everytime the location changes.
1componentDidMount() { 2 this.watchId = navigator.geolocation.watchPosition( 3 (position) => { 4 5 var region = regionFrom( 6 position.coords.latitude, 7 position.coords.longitude, 8 position.coords.accuracy 9 ); 10 // update the UI 11 this.setState({ 12 region: region, 13 accuracy: position.coords.accuracy 14 }); 15 16 if(this.state.has_passenger && this.state.passenger){ 17 // next: add code for sending driver's current location to passenger 18 } 19 }, 20 (error) => this.setState({ error: error.message }), 21 { 22 enableHighAccuracy: true, // allows you to get the most accurate location 23 timeout: 20000, // (milliseconds) in which the app has to wait for location before it throws an error 24 maximumAge: 1000, // (milliseconds) if a previous location exists in the cache, how old for it to be considered acceptable 25 distanceFilter: 10 // (meters) how many meters the user has to move before a location update is triggered 26 }, 27 ); 28 }
Next, send the driver’s current location to the passenger. This will update the UI on the passenger app to show the current location of the driver. You’ll see how the passenger app binds to this event later on when we move on to coding the passenger app.
1this.ride_channel.trigger('client-driver-location', { 2 latitude: position.coords.latitude, 3 longitude: position.coords.longitude, 4 accuracy: position.coords.accuracy 5 });
Next, we want to inform both the passenger and the driver that they’re already near each other. For that, we use the getLatLonDiffInMeters
function from the helpers.js
file in order to determine the number of meters between the passenger and the driver. Since the driver already received the passenger location when they accepted the request, it’s only a matter of getting the current location of the driver and passing it to the getLanLonDiffInMeters
function to get the difference in meters. From there, we simply inform the driver or the passenger based on the number of meters. Later on you’ll see how these events are received in the passenger app.
1var diff_in_meter_pickup = getLatLonDiffInMeters( 2 position.coords.latitude, position.coords.longitude, 3 this.state.passenger.pickup.latitude, this.state.passenger.pickup.longitude); 4 5 if(diff_in_meter_pickup <= 20){ 6 7 if(!this.state.has_ridden){ 8 // inform the passenger that the driver is very near 9 this.ride_channel.trigger('client-driver-message', { 10 type: 'near_pickup', 11 title: 'Just a heads up', 12 msg: 'Your driver is near, let your presence be known!' 13 }); 14 15 /* 16 we're going to go ahead and assume that the passenger has rode 17 the vehicle at this point 18 */ 19 this.setState({ 20 has_ridden: true 21 }); 22 } 23 24 }else if(diff_in_meter_pickup <= 50){ 25 26 if(!this.state.nearby_alert){ 27 this.setState({ 28 nearby_alert: true 29 }); 30 /* 31 since the location updates every 10 meters, this alert will be triggered 32 at least five times unless we do this 33 */ 34 Alert.alert( 35 "Slow down", 36 "Your passenger is just around the corner", 37 [ 38 { 39 text: 'Gotcha!' 40 }, 41 ], 42 { cancelable: false } 43 ); 44 45 } 46 47 } 48 49 // next: add code for sending messages when near the destination
At this point, we assume that the driver has picked up the passenger and that they’re now heading to their destination. So this time we get the distance between the current location and the drop-off point. Once they’re 20 meters to the drop-off point, the driver app sends a message to the passenger that they’re very close to their destination. Once that’s done, we assume that the passenger will get off in a few seconds. So we unbind the events that we’re listening to and unsubscribe from the passenger’s private channel. This effectively cuts the connection between the driver and passenger app. The only connection that stays open is the available-drivers
channel.
1var diff_in_meter_dropoff = getLatLonDiffInMeters( 2 position.coords.latitude, position.coords.longitude, 3 this.state.passenger.dropoff.latitude, this.state.passenger.dropoff.longitude); 4 5 if(diff_in_meter_dropoff <= 20){ 6 this.ride_channel.trigger('client-driver-message', { 7 type: 'near_dropoff', 8 title: "Brace yourself", 9 msg: "You're very close to your destination. Please prepare your payment." 10 }); 11 12 // unbind from passenger event 13 this.ride_channel.unbind('client-driver-response'); 14 // unsubscribe from passenger channel 15 this.pusher.unsubscribe('private-ride-' + this.state.passenger.username); 16 17 this.setState({ 18 passenger: null, 19 has_passenger: false, 20 has_ridden: false 21 }); 22 23 } 24 25 // next: add code for rendering the UI
The UI for the driver app only displays the map and the markers for the driver and passenger.
1render() { 2 return ( 3 <View style={styles.container}> 4 { 5 this.state.region && 6 <MapView 7 style={styles.map} 8 region={this.state.region} 9 > 10 <MapView.Marker 11 coordinate={{ 12 latitude: this.state.region.latitude, 13 longitude: this.state.region.longitude}} 14 title={"You're here"} 15 /> 16 { 17 this.state.passenger && !this.state.has_ridden && 18 <MapView.Marker 19 coordinate={{ 20 latitude: this.state.passenger.pickup.latitude, 21 longitude: this.state.passenger.pickup.longitude}} 22 title={"Your passenger is here"} 23 pinColor={"#4CDB00"} 24 /> 25 } 26 </MapView> 27 } 28 </View> 29 ); 30 } 31 // next: add code when component unmounts
Before the component unmounts, we stop the location watcher by calling the clearWatch
method:
1componentWillUnmount() { 2 navigator.geolocation.clearWatch(this.watchId); 3 }
Lastly, add the styles:
1const styles = StyleSheet.create({ 2 container: { 3 ...StyleSheet.absoluteFillObject, 4 justifyContent: 'flex-end', 5 alignItems: 'center', 6 }, 7 map: { 8 ...StyleSheet.absoluteFillObject, 9 }, 10 });
The passenger app is going to be pretty similar to the driver app so I’ll no longer go into detail on parts that are similar. Go ahead and create a new app:
react-native init grabClone
You’d also need to install the same libraries plus a couple more:
npm install --save pusher-js react-native-geocoding github:geordasche/react-native-google-place-picker react-native-loading-spinner-overlay react-native-maps
The other two libraries are Google Place Picker and Loading Spinner Overlay. Though we’ve used a fork of the Google Place Picker because of a compatibility issue with React Native Maps that wasn’t fixed in the original repo yet.
Since we’ve installed the same libraries, you can go back to the section where we did some additional configuration in order for the library to work. Come back here once you’ve done those.
Next, the Google Place Picker also needs some additional configuration for it to work. First, open the android/app/src/main/java/com/grabClone/MainApplication.java
file and add the following below the last import:
import com.reactlibrary.RNGooglePlacePickerPackage;
Add the library that you just imported under the getPackages()
function. While you’re there, also make sure that the MapsPackage()
is listed as well.
1protected List<ReactPackage> getPackages() { 2 return Arrays.<ReactPackage>asList( 3 new MainReactPackage(), 4 new MapsPackage(), 5 new RNGooglePlacePickerPackage() // <- add this 6 ); 7 }
Next, open the android/settings.gradle
file and add these right above the include ':app'
directive:
1include ':react-native-google-place-picker' 2 project(':react-native-google-place-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-place-picker/android')
While you’re there, also make sure that the resources for React Native Maps are also added:
1include ':react-native-maps' 2 project(':react-native-maps').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-maps/lib/android')
Next, open the android/app/build.gradle
file and add the following under the dependencies
:
1dependencies { 2 compile project(':react-native-google-place-picker') // <- add this 3 }
Lastly, make sure that React Native Maps is also compiled:
compile project(':react-native-maps')
Open the index.android.js
file and add the following:
1import { AppRegistry } from 'react-native'; 2 import App from './App'; 3 AppRegistry.registerComponent('grabClone', () => App);
Just like the driver app, it also uses App.js
as the main component. Go ahead and import the libraries. It also uses the same helpers.js
file so you can copy it from the driver app as well.
1import React, { Component } from 'react'; 2 import { StyleSheet, Text, View, Button, Alert } from 'react-native'; 3 4 import Pusher from 'pusher-js/react-native'; 5 import RNGooglePlacePicker from 'react-native-google-place-picker'; 6 import Geocoder from 'react-native-geocoding'; 7 import MapView from 'react-native-maps'; 8 import Spinner from 'react-native-loading-spinner-overlay'; 9 10 import { regionFrom, getLatLonDiffInMeters } from './helpers'; 11 12 Geocoder.setApiKey('YOUR GOOGLE SERVER API KEY');
Create the component and declare the default states:
1export default class App extends Component { 2 state = { 3 location: null, // current location of the passenger 4 error: null, // for storing errors 5 has_ride: false, // whether the passenger already has a driver which accepted their request 6 destination: null, // for storing the destination / dropoff info 7 driver: null, // the driver info 8 origin: null, // for storing the location where the passenger booked a ride 9 is_searching: false, // if the app is currently searching for a driver 10 has_ridden: false // if the passenger has already been picked up by the driver 11 }; 12 13 // next: add constructor code 14 }
To keep things simple, we declare the username of the passenger in the constructor. We also initialize the Pusher channels:
1constructor() { 2 super(); 3 this.username = 'wernancheta'; // the unique username of the passenger 4 this.available_drivers_channel = null; // the pusher channel where all drivers and passengers are subscribed to 5 this.user_ride_channel = null; // the pusher channel exclusive to the passenger and driver in a given ride 6 this.bookRide = this.bookRide.bind(this); // bind the function for booking a ride 7 } 8 // next: add bookRide() function
The bookRide()
function gets executed when the user taps on the “Book Ride” button. This opens a place picker which allows the user to pick their destination. Once a location is picked, the app sends a ride request to all drivers. As you have seen in the driver app earlier, this triggers an alert to show in the driver app which asks if the driver wants to accept the request or not. At this point the loader will keep on spinning until a driver accepts the request.
1bookRide() { 2 3 RNGooglePlacePicker.show((response) => { 4 if(response.didCancel){ 5 console.log('User cancelled GooglePlacePicker'); 6 }else if(response.error){ 7 console.log('GooglePlacePicker Error: ', response.error); 8 }else{ 9 this.setState({ 10 is_searching: true, // show the loader 11 destination: response // update the destination, this is used in the UI to display the name of the place 12 }); 13 14 // the pickup location / origin 15 let pickup_data = { 16 name: this.state.origin.name, 17 latitude: this.state.location.latitude, 18 longitude: this.state.location.longitude 19 }; 20 21 // the dropoff / destination 22 let dropoff_data = { 23 name: response.name, 24 latitude: response.latitude, 25 longitude: response.longitude 26 }; 27 28 // send a ride request to all drivers 29 this.available_drivers_channel.trigger('client-driver-request', { 30 username: this.username, 31 pickup: pickup_data, 32 dropoff: dropoff_data 33 }); 34 35 } 36 }); 37 } 38 // next: add _setCurrentLocation() function
The _setCurrentLocation()
function gets the passenger’s current location. Note that here we’re using getCurrentPosition()
as opposed to watchPosition()
which we used in the driver app earlier. The only difference between the two is that getCurrentPosition()
only gets the location once.
1_setCurrentLocation() { 2 3 navigator.geolocation.getCurrentPosition( 4 (position) => { 5 var region = regionFrom( 6 position.coords.latitude, 7 position.coords.longitude, 8 position.coords.accuracy 9 ); 10 11 // get the name of the place by supplying the coordinates 12 Geocoder.getFromLatLng(position.coords.latitude, position.coords.longitude).then( 13 (json) => { 14 var address_component = json.results[0].address_components[0]; 15 16 this.setState({ 17 origin: { // the passenger's current location 18 name: address_component.long_name, // the name of the place 19 latitude: position.coords.latitude, 20 longitude: position.coords.longitude 21 }, 22 location: region, // location to be used for the Map 23 destination: null, 24 has_ride: false, 25 has_ridden: false, 26 driver: null 27 }); 28 29 }, 30 (error) => { 31 console.log('err geocoding: ', error); 32 } 33 ); 34 35 }, 36 (error) => this.setState({ error: error.message }), 37 { enableHighAccuracy: false, timeout: 10000, maximumAge: 3000 }, 38 ); 39 40 } 41 42 // next: add componentDidMount() function
When the component mounts, we want to set the current location of the passenger, connect to the auth server and subscribe to the two channels: available drivers and the passenger’s private channel for communicating only with the driver’s where the ride request was sent to.
1componentDidMount() { 2 3 this._setCurrentLocation(); // set current location of the passenger 4 // connect to the auth server 5 var pusher = new Pusher('YOUR PUSHER API KEY', { 6 authEndpoint: 'YOUR AUTH SERVER ENDPOINT', 7 cluster: 'YOUR PUSHER CLUSTER', 8 encrypted: true 9 }); 10 11 // subscribe to the available drivers channel 12 this.available_drivers_channel = pusher.subscribe('private-available-drivers'); 13 14 // subscribe to the passenger's private channel 15 this.user_ride_channel = pusher.subscribe('private-ride-' + this.username); 16 17 // next: add code for listening to handshake responses 18 19 }
Next, add the code for listening to the handshake response by the driver. This is being sent from the driver app when the driver accepts a ride request. This allows us to make sure that the passenger is still looking for a ride. If the passenger responds with “yes” then that’s the only time that the driver sends their information.
1this.user_ride_channel.bind('client-driver-response', (data) => { 2 let passenger_response = 'no'; 3 if(!this.state.has_ride){ // passenger is still looking for a ride 4 passenger_response = 'yes'; 5 } 6 7 // passenger responds to driver's response 8 this.user_ride_channel.trigger('client-driver-response', { 9 response: passenger_response 10 }); 11 }); 12 13 // next: add listener for when a driver is found
The driver sends their information by triggering the client-found-driver
event. As you have seen in the driver app earlier, this contains the name of the driver as well as their current location.
1this.user_ride_channel.bind('client-found-driver', (data) => { 2 // the driver's location info 3 let region = regionFrom( 4 data.location.latitude, 5 data.location.longitude, 6 data.location.accuracy 7 ); 8 9 this.setState({ 10 has_ride: true, // passenger has already a ride 11 is_searching: false, // stop the loading UI from spinning 12 location: region, // display the driver's location in the map 13 driver: { // the driver location details 14 latitude: data.location.latitude, 15 longitude: data.location.longitude, 16 accuracy: data.location.accuracy 17 } 18 }); 19 20 // alert the passenger that a driver was found 21 Alert.alert( 22 "Orayt!", 23 "We found you a driver. \nName: " + data.driver.name + "\nCurrent location: " + data.location.name, 24 [ 25 { 26 text: 'Sweet!' 27 }, 28 ], 29 { cancelable: false } 30 ); 31 }); 32 // next: add code for listening to driver's current location
At this point, the passenger can now listen to location changes from the driver. We simply update the UI everytime this event is triggered:
1this.user_ride_channel.bind('client-driver-location', (data) => { 2 let region = regionFrom( 3 data.latitude, 4 data.longitude, 5 data.accuracy 6 ); 7 8 // update the Map to display the current location of the driver 9 this.setState({ 10 location: region, // the driver's location 11 driver: { 12 latitude: data.latitude, 13 longitude: data.longitude 14 } 15 }); 16 17 });
Next is the event that is triggered on specific instances. It’s main purpose is to send updates to the passenger regarding the location of the driver (near_pickup
) and also when they’re already near the drop-off location (near_dropoff
).
1this.user_ride_channel.bind('client-driver-message', (data) => { 2 if(data.type == 'near_pickup'){ // the driver is very near the pickup location 3 // remove passenger marker since we assume that the passenger has rode the vehicle at this point 4 this.setState({ 5 has_ridden: true 6 }); 7 } 8 9 if(data.type == 'near_dropoff'){ // they're near the dropoff location 10 this._setCurrentLocation(); // assume that the ride is over, so reset the UI to the current location of the passenger 11 } 12 13 // display the message sent from the driver app 14 Alert.alert( 15 data.title, 16 data.msg, 17 [ 18 { 19 text: 'Aye sir!' 20 }, 21 ], 22 { cancelable: false } 23 ); 24 }); 25 26 // next: render the UI
The UI composed of the loading spinner (only visible when the app is searching for a driver), the header, the button for booking a ride, the passenger location (origin
) and their destination, and the map which initially displays the current location of the user and then displays the current location of the driver once a ride has been booked.
1render() { 2 3 return ( 4 <View style={styles.container}> 5 <Spinner 6 visible={this.state.is_searching} 7 textContent={"Looking for drivers..."} 8 textStyle={{color: '#FFF'}} /> 9 <View style={styles.header}> 10 <Text style={styles.header_text}>GrabClone</Text> 11 </View> 12 { 13 !this.state.has_ride && 14 <View style={styles.form_container}> 15 <Button 16 onPress={this.bookRide} 17 title="Book a Ride" 18 color="#103D50" 19 /> 20 </View> 21 } 22 23 <View style={styles.map_container}> 24 { 25 this.state.origin && this.state.destination && 26 <View style={styles.origin_destination}> 27 <Text style={styles.label}>Origin: </Text> 28 <Text style={styles.text}>{this.state.origin.name}</Text> 29 30 <Text style={styles.label}>Destination: </Text> 31 <Text style={styles.text}>{this.state.destination.name}</Text> 32 </View> 33 } 34 { 35 this.state.location && 36 <MapView 37 style={styles.map} 38 region={this.state.location} 39 > 40 { 41 this.state.origin && !this.state.has_ridden && 42 <MapView.Marker 43 coordinate={{ 44 latitude: this.state.origin.latitude, 45 longitude: this.state.origin.longitude}} 46 title={"You're here"} 47 /> 48 } 49 50 { 51 this.state.driver && 52 <MapView.Marker 53 coordinate={{ 54 latitude: this.state.driver.latitude, 55 longitude: this.state.driver.longitude}} 56 title={"Your driver is here"} 57 pinColor={"#4CDB00"} 58 /> 59 } 60 </MapView> 61 } 62 </View> 63 </View> 64 ); 65 }
Lastly, add the styles:
1const styles = StyleSheet.create({ 2 container: { 3 ...StyleSheet.absoluteFillObject, 4 justifyContent: 'flex-end' 5 }, 6 form_container: { 7 flex: 1, 8 justifyContent: 'center', 9 padding: 20 10 }, 11 header: { 12 padding: 20, 13 backgroundColor: '#333', 14 }, 15 header_text: { 16 color: '#FFF', 17 fontSize: 20, 18 fontWeight: 'bold' 19 }, 20 origin_destination: { 21 alignItems: 'center', 22 padding: 10 23 }, 24 label: { 25 fontSize: 18 26 }, 27 text: { 28 fontSize: 18, 29 fontWeight: 'bold', 30 }, 31 map_container: { 32 flex: 9 33 }, 34 map: { 35 flex: 1 36 }, 37 });
Now you’re ready to run the app. If you have two machines, this will allow you to enable logging (console.log
) for both. But if you have only one machine then you have to run them in particular order: passenger app first and then driver app.
Go ahead and connect your Android device to your computer and run the following command:
react-native run-android
This will compile, install and run the app on your device. Once its running, terminate the watcher and disconnect your device from the computer.
Next, open Genymotion and launch the device that you installed earlier. This time, run the driver app. Once the app runs you’ll see a blank screen. This is normal because the app needs a location in order to render something. You can do that by clicking on “GPS” located on the upper right-side of the emulator UI then enable GPS.
You can also click on the map button and select a specific location if you want:
Once you’ve selected a location, the map UI in the app should show the same location that you selected.
Next, you can now follow the steps on the App Flow section earlier. Note that you can emulate a moving vehicle by clicking around the Genymotion Map UI. If a passenger has already booked a ride and the driver has accepted the request, it should start updating both the passenger app and the driver app of the current location of the driver.
If you’re using two machines, then you can simply run react-native run-android
on both. One should be connected to your device and the other should have the Genymotion emulator open.
That’s it! In this tutorial you’ve learned how to make use of Pusher to create a ride hailing app. As you have seen, the app that you’ve built is pretty bare-bones. If you want you can add more features to the app and maybe use it on your own projects. The code for the completed app is available on GitHub