In this tutorial we’ll create a ride-booking app with React Native and Pusher. The app that we’ll create will be similar to popular ride-booking apps like Uber, Lyft or Grab. React Native will be used to create an Android app for both the driver and the passenger. Pusher will be used for real-time communication between the two. What You’ll Create Just like any other ride-booking app out there, there will be a driver app and a passenger app. The passenger app will be used for booking a ride, and the driver app simply receives any request that comes from the passenger app. For consistency purposes we’ll just refer to the app as “grabClone”. App Flow The clone that we’re going to create will pretty much have the same flow as any ride booking 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. Here I just want to show you how this process would look like inside the app. That way you’ll have a clear picture of what you’re going to create. 1. The app determines the user’s location and shows it in a map (note: GPS needs to be enabled at this point). 2. From the passenger app, the user clicks on “Book a Ride”. 3. A modal will open that will allow the passenger to pick the place where they want to go. 4. The app asks the passenger to confirm their destination. 5. Once confirmed, the app sends a request to the driver app to pick up the passenger. A loading animation is shown while the app is waiting for a driver to accept the request. 6. The driver app receives the request. From here, the driver can either accept or reject the request. 7. Once the driver accepts the request, the driver’s details are shown in the passenger app. 8. The passenger app shows the current location of the driver on the map. 9. Once the driver is within 50 meters of the passenger’s location, they will see an alert saying that the driver is near. 10. Once the driver is within 20 meters of the passenger’s location, the driver app sends a message to the passenger app that the driver is almost there. 11. After picking up the passenger, the driver drives to their destination. 12. Once the driver is within 20 meters of their destination, the driver app sends a message to the passenger app that they’re very near their destination. At this point, the ride ends and the passenger can book another ride. The driver is also free to accept any incoming ride request. Prerequisites 1. Pusher Account or . Once you’ve created an account, create a new app → select “React” for front-end technology → select “Node.js” for back-end technology. Sign up for a Pusher account login with your existing one 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. Lastly, click on the “App keys” and copy the credentials. If you’re worried about pricing, so you can use it for free when testing the app. the Pusher sandbox plan is pretty generous 2. Android Studio You don’t really need Android Studio, but it comes with Android SDK which is the one we need. Google also no longer offers a separate download for it. 3. React Native The method I recommend for this is building projects the native way. When you’re on the React Native website, click on the “Building Projects with Native Code” tab and follow the instructions in there. The client is great for quickly prototyping apps but it doesn’t really offer a quick way for us to test the Geolocation features that we need for this app. Expo 4. Genymotion We use for testing the driver app. We’re using this instead of the default Android emulator because it comes with a GPS simulation tool that allows us to search for a specific location and have it used as the location of the emulated device. It uses Google maps as the interface and you can move the marker as well. This allows us to simulate a moving vehicle. Genymotion Once Genymotion is installed, you need to login to your account in order to add a device. For me I’ve installed Google Nexus 5x for testing. 5. Android Device This will be used for testing the passenger app. Be sure to check the Android version of your phone. If it’s something as low as 4.2 then you’ll need to install additional packages via the Android SDK Manager. This is because, by default, React Native targets API version 23 or higher. That means the Android version of your phone needs to be version 6.0 at the very least or the app won’t run. If you have installed Android Studio, you can access the SDK Manager by opening Android Studio → click “Configure” → select “SDK Manager“. Then under the “SDK Platforms”, check the Android versions that you want to support. While you’re there, click on the “SDK Tools” and make sure that you also have the same tools installed as mine: 6. An extra computer (optional) This is optional. I just included it here because React Native can only run the app on a single device or emulator at a time. Thus, you need to do some extra work in order to run the two apps as you’ll see later on. Creating the Auth Server Now it’s time to get our hands dirty. First, let’s work on the auth server. This is required because we will be sending 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. client events Start by installing the dependencies: npm install --save express body-parser pusher Next, create a file and add the following code: server.js express = ( ); bodyParser = ( ); Pusher = ( ); app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ : })); pusher = Pusher({ appId: process.env.APP_ID, : process.env.APP_KEY, : process.env.APP_SECRET, : process.env.APP_CLUSTER, }); app.get( , { res.send( ); }); app.get( , { query = req.query; socketId = query.socket_id; channel = query.channel_name; callback = query.callback; auth = .stringify(pusher.authenticate(socketId, channel)); cb = callback.replace( , ) + + auth + ; res.set({ : }); res.send(cb); }); app.post( , { socketId = req.body.socket_id; channel = req.body.channel_name; auth = pusher.authenticate(socketId, channel); res.send(auth); }); port = process.env.PORT || ; app.listen(port); var require 'express' var require 'body-parser' var require 'pusher' var extended false var new // connect to pusher key secret cluster '/' ( ) function req, res // for testing if the server is running 'all is well...' // for authenticating users "/pusher/auth" ( ) function req, res var var var var var JSON var /\"/g "" "(" ");" "Content-Type" "application/javascript" '/pusher/auth' ( ) function req, res var var var var 5000 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 actually added the code to check if a user really exists in a database. You can do that in the endpoint by checking if a username exists. Here’s an example: /pusher/auth var users = [ , , ]; var username = req.body.username; (users.indexOf(username) !== -1){ var socketId = req.body.socket_id; var channel = req.body.channel_name; var auth = pusher.authenticate(socketId, channel); res.send(auth); } // otherwise: error 'luz' 'vi' 'minda' if return Don’t forget to pass in the when connecting to Pusher on the client-side later on. username Try running the server once that’s done: node server.js Access on your browser to see if it works. http://localhost:5000 Deploying the Auth Server Since Pusher will have to connect to the auth server, it needs to be accessible from the internet. You can use to deploy the auth server. You can install it with the following command: now.sh npm install now Once installed, you can now navigate to the folder where you have the file and execute . You’ll be asked to enter your email and verify your account. server.js now 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: now secret add pusher_app_id YOUR_PUSHER_APP_ID now secret add pusher_app_key YOUR_PUSHER_APP_KEY now secret add pusher_app_secret YOUR_PUSHER_APP_SECRET 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. Creating the Driver App Now you’re ready to start creating the driver app. First, create a new React Native app: react-native init grabDriver Installing the dependencies Once that’s done, navigate inside the directory and install the libraries that we’ll need. This includes for working with Pusher, for displaying a map, and for reverse-geocoding coordinates to the actual name of a place: grabDriver pusher-js React Native Maps React Native Geocoding 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 , and enable the and . After that, open the file in your project directory. Under the tag, add a containing the server API key Google developer console Google Maps Android API Google Maps Geocoding API android\app\src\main\AndroidManifest.xml <application> <meta-data> < > application < = = /> meta-data android:name "com.google.android.geo.API_KEY" android:value "YOUR GOOGLE SERVER API KEY" </ > 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. < = /> uses-permission android:name "android.permission.ACCESS_NETWORK_STATE" < = /> uses-permission android:name "android.permission.ACCESS_FINE_LOCATION" Also make sure that its 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. < = = /> uses-sdk android:minSdkVersion "16" android:targetSdkVersion "23" Lastly, since we’ll be primarily using Genymotion for testing the driver app, you need to follow the . In case the link goes broken, here’s what you need to do: instructions here Visit . opengapps.org Select x86 as platform. Choose the Android version corresponding to your virtual device. Select nano as variant. Download the zip file. Drag & Drop the zip installer in new Genymotion virtual device (2.7.2 and above only). Follow the pop-up instructions. 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 some time after it was published, be sure to check out the to make sure you’re not missing anything. Installation docs Coding the driver app Now you’re ready to start coding the app. Start by opening the file and replace the default code with the following: index.android.js { AppRegistry } ; App ; AppRegistry.registerComponent( , () => App); import from 'react-native' import from './App' 'grabDriver' What this does is importing the component which is the main component for the app. It is then registered as the default component so it will be rendered in the screen. App Next, create the file and import the things we need from the React Native package: App.js React, { Component } ; { StyleSheet, Text, View, Alert } ; import from 'react' import from 'react-native' Also import the third-party libraries that we installed earlier: Pusher ; MapView ; Geocoder ; Geocoder.setApiKey( ); import from 'pusher-js/react-native' import from 'react-native-maps' import from 'react-native-geocoding' 'YOUR GOOGLE SERVER API KEY' Lastly, import the file: helpers { regionFrom, getLatLonDiffInMeters } ; import from './helpers' The file contains the following: helpers.js { oneDegreeOfLongitudeInMeters = * ; circumference = ( / ) * ; latDelta = accuracy * ( / ( .cos(lat) * circumference)); lonDelta = (accuracy / oneDegreeOfLongitudeInMeters); { : lat, : lon, : .max( , latDelta), : .max( , lonDelta) }; } { R = ; dLat = deg2rad(lat2-lat1); dLon = deg2rad(lon2-lon1); a = .sin(dLat/ ) * .sin(dLat/ ) + .cos(deg2rad(lat1)) * .cos(deg2rad(lat2)) * .sin(dLon/ ) * .sin(dLon/ ) ; c = * .atan2( .sqrt(a), .sqrt( -a)); d = R * c; d * ; } { deg * ( .PI/ ) } export ( ) function regionFrom lat, lon, accuracy const 111.32 1000 const 40075 360 1000 const 1 Math const return latitude longitude latitudeDelta Math 0 longitudeDelta Math 0 export ( ) function getLatLonDiffInMeters lat1, lon1, lat2, lon2 var 6371 // Radius of the earth in km var // deg2rad below var var Math 2 Math 2 Math Math Math 2 Math 2 var 2 Math Math Math 1 var // Distance in km return 1000 ( ) function deg2rad deg return Math 180 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 ( ) 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. getLatLonDiffInMeters Next, create the main app component and declare the default states: { state = { : , region: , accuracy: , nearby_alert: , has_passenger: , has_ridden: } } export default class grabDriver extends Component passenger null // for storing the passenger info null // for storing the current location of the driver null // for storing the accuracy of the location false // whether the nearby alert has already been issued false // whether the driver has a passenger (once they agree to a request, this becomes true) false // whether the passenger has already ridden the vehicle // next: add constructor code Inside the constructor, initialize the variables that will be used throughout the app: () { (); .available_drivers_channel = ; .ride_channel = ; .pusher = ; } constructor super this null // this is where passengers will send a request to any available driver this null // the channel used for communicating the current location // for a specific ride. Channel name is the username of the passenger this null // the pusher client // 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, and . authEndpoint cluster componentWillMount() { .pusher = Pusher( , { : , : , : }); } this new 'YOUR PUSHER KEY' authEndpoint 'YOUR PUSHER AUTH SERVER ENDPOINT' cluster 'YOUR PUSHER CLUSTER' encrypted true // next: add code for listening to passenger requests 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 . This channel is where all passengers and drivers subscribe to. In this case, it's used by drivers to listen for ride requests. It needs to be a private channel because can only be triggered on private and presence channels due to security reasons. You know that it’s a private channel because of the prefix. private channel client events private- .available_drivers_channel = .pusher.subscribe( ); this this 'private-available-drivers' // subscribe to "available-drivers" channel Next, listen to the event. You know that this is a client event because of the prefix. Client events doesn’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. client-driver-request client- Going back to the code, we listen for client events by calling the 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. bind .available_drivers_channel.bind( , (passenger_data) => { (! .state.has_passenger){ Alert.alert( , + passenger_data.pickup.name + + passenger_data.dropoff.name, [ { : , onPress: { .log( ); }, : }, { : , onPress: { } }, ], { : } ); } }); // listen to the "driver-request" event this 'client-driver-request' if this // if the driver has currently no passenger // alert the driver that they have a request "You got a passenger!" // alert title "Pickup: " "\nDrop off: " // alert body text "Later bro" // text for rejecting the request => () console 'Cancel Pressed' style 'cancel' text 'Gotcha!' // text for accepting the request => () // next: add code for when driver accepts the request cancelable false // no cancel button 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. .ride_channel = .pusher.subscribe( + passenger_data.username); this this 'private-ride-' Not unlike the channel, we’ll need to listen for when the subscription actually 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 ( ) 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. available-drivers pusher:subscription_succeeded client-driver-response .ride_channel.bind( , () => { .ride_channel.trigger( , { : }); .ride_channel.bind( , (driver_response) => { (driver_response.response == ){ .setState({ : , : { : passenger_data.username, : passenger_data.pickup, : passenger_data.dropoff } }); } { Alert.alert( , , [ { : }, ], { : } ); } }); }); this 'pusher:subscription_succeeded' // send a handshake event to the passenger this 'client-driver-response' response 'yes' // yes, I'm available // listen for the acknowledgement from the passenger this 'client-driver-response' if 'yes' // passenger says yes //passenger has no ride yet this has_passenger true passenger username pickup dropoff // next: reverse-geocode the driver location to the actual name of the place else // alert that passenger already has a ride "Too late bro!" "Another driver beat you to it." text 'Ok' cancelable false 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 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. found-driver Geocoder.getFromLatLng( .state.region.latitude, .state.region.longitude).then( { address_component = json.results[ ].address_components[ ]; .ride_channel.trigger( , { : { : }, : { : address_component.long_name, : .state.region.latitude, : .state.region.longitude, : .state.accuracy } }); }, (error) => { .log( , error); } ); this this ( ) => json var 0 0 // inform passenger that it has found a driver this 'client-found-driver' driver name 'John Smith' location name latitude this longitude this accuracy this console 'err geocoding: ' // next: add componentDidMount code Once the component is mounted, we use to watch for location updates. The function that you pass to the function gets executed everytime the location changes. React Native’s Geolocation API watchPosition componentDidMount() { .watchId = navigator.geolocation.watchPosition( { region = regionFrom( position.coords.latitude, position.coords.longitude, position.coords.accuracy ); .setState({ : region, : position.coords.accuracy }); ( .state.has_passenger && .state.passenger){ } }, (error) => .setState({ : error.message }), { : , timeout: , maximumAge: , distanceFilter: }, ); } this ( ) => position var // update the UI this region accuracy if this this // next: add code for sending driver's current location to passenger this error enableHighAccuracy true // allows you to get the most accurate location 20000 // (milliseconds) in which the app has to wait for location before it throws an error 1000 // (milliseconds) if a previous location exists in the cache, how old for it to be considered acceptable 10 // (meters) how many meters the user has to move before a location update is triggered 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. .ride_channel.trigger( , { : position.coords.latitude, : position.coords.longitude, : position.coords.accuracy }); this 'client-driver-location' latitude longitude accuracy Next, we want to inform both the passenger and the driver that they’re already near each other. For that we use the function from the 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 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. getLatLonDiffInMeters helpers.js getLanLonDiffInMeters diff_in_meter_pickup = getLatLonDiffInMeters( position.coords.latitude, position.coords.longitude, .state.passenger.pickup.latitude, .state.passenger.pickup.longitude); (diff_in_meter_pickup <= ){ (! .state.has_ridden){ .ride_channel.trigger( , { : , : , : }); .setState({ : }); } } (diff_in_meter_pickup <= ){ (! .state.nearby_alert){ .setState({ : }); Alert.alert( , , [ { : }, ], { : } ); } } var this this if 20 if this // inform the passenger that the driver is very near this 'client-driver-message' type 'near_pickup' title 'Just a heads up' msg 'Your driver is near, let your presence be known!' /* we're going to go ahead and assume that the passenger has rode the vehicle at this point */ this has_ridden true else if 50 if this this nearby_alert true /* since the location updates every 10 meters, this alert will be triggered at least five times unless we do this */ "Slow down" "Your passenger is just around the corner" text 'Gotcha!' cancelable false // 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 channel. available-drivers diff_in_meter_dropoff = getLatLonDiffInMeters( position.coords.latitude, position.coords.longitude, .state.passenger.dropoff.latitude, .state.passenger.dropoff.longitude); (diff_in_meter_dropoff <= ){ .ride_channel.trigger( , { : , : , : }); .ride_channel.unbind( ); .pusher.unsubscribe( + .state.passenger.username); .setState({ : , : , : }); } var this this if 20 this 'client-driver-message' type 'near_dropoff' title "Brace yourself" msg "You're very close to your destination. Please prepare your payment." // unbind from passenger event this 'client-driver-response' // unsubscribe from passenger channel this 'private-ride-' this this passenger null has_passenger false has_ridden false // 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. render() { ( <MapView style={styles.map} region={this.state.region} > <MapView.Marker coordinate={{ latitude: this.state.region.latitude, longitude: this.state.region.longitude}} title={"You're here"} /> { this.state.passenger && !this.state.has_ridden && <MapView.Marker coordinate={{ latitude: this.state.passenger.pickup.latitude, longitude: this.state.passenger.pickup.longitude}} title={"Your passenger is here"} pinColor={"#4CDB00"} /> } </MapView> } </View> ); } // next: add code when component unmounts return { this.state.region && < = > View style {styles.container} Before the component unmounts, we stop the location watcher by calling the method: clearWatch componentWillUnmount() { navigator.geolocation.clearWatch( .watchId); } this Lastly, add the styles: styles = StyleSheet.create({ : { ...StyleSheet.absoluteFillObject, : , : , }, : { ...StyleSheet.absoluteFillObject, }, }); const container justifyContent 'flex-end' alignItems 'center' map Creating the Passenger App 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 Installing the Dependencies 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 and . Though we’ve used a of the Google Place Picker because of a compatibility issue with React Native Maps that wasn’t fixed in the original repo yet. Google Place Picker Loading Spinner Overlay fork 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 file and add the following below the last import: android/app/src/main/java/com/grabClone/MainApplication.java com.reactlibrary.RNGooglePlacePickerPackage; import Add the library that you just imported under the function. While you’re there, also make sure that the is listed as well. getPackages() MapsPackage() protected List<ReactPackage> getPackages() { Arrays.<ReactPackage>asList( MainReactPackage(), MapsPackage(), RNGooglePlacePickerPackage() ); } return new new new // <- add this Next, open the file and add these right above the directive: android/settings.gradle include ':app' include project( ).projectDir = File(rootProject.projectDir, ) ':react-native-google-place-picker' ':react-native-google-place-picker' new '../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: include project( ).projectDir = File(rootProject.projectDir, ) ':react-native-maps' ':react-native-maps' new '../node_modules/react-native-maps/lib/android' Next, open the file and add the following under the : android/app/build.gradle dependencies dependencies { compile } project ( ) ':react-native-google-place-picker' // <- add this Lastly, make sure that React Native Maps is also compiled: compile project ( ) ':react-native-maps' Coding the Passenger App Open the file and add the following: index.android.js { AppRegistry } ; App ; AppRegistry.registerComponent( , () => App); import from 'react-native' import from './App' 'grabClone' Just like the driver app, it also uses as the main component. Go ahead and import the libraries. It also uses the same file so you can copy it from the driver app as well. App.js helpers.js React, { Component } ; { StyleSheet, Text, View, Button, Alert } ; Pusher ; RNGooglePlacePicker ; Geocoder ; MapView ; Spinner ; { regionFrom, getLatLonDiffInMeters } ; Geocoder.setApiKey( ); import from 'react' import from 'react-native' import from 'pusher-js/react-native' import from 'react-native-google-place-picker' import from 'react-native-geocoding' import from 'react-native-maps' import from 'react-native-loading-spinner-overlay' import from './helpers' 'YOUR GOOGLE SERVER API KEY' Create the component and declare the default states: { state = { : , error: , has_ride: , destination: , driver: , origin: , is_searching: , has_ridden: }; } export default class App extends Component location null // current location of the passenger null // for storing errors false // whether the passenger already has a driver which accepted their request null // for storing the destination / dropoff info null // the driver info null // for storing the location where the passenger booked a ride false // if the app is currently searching for a driver false // if the passenger has already been picked up by the driver // next: add constructor code To keep things simple, we declare the username of the passenger in the constructor. We also initialize the Pusher channels: () { (); .username = ; .available_drivers_channel = ; .user_ride_channel = ; .bookRide = .bookRide.bind( ); } constructor super this 'wernancheta' // the unique username of the passenger this null // the pusher channel where all drivers and passengers are subscribed to this null // the pusher channel exclusive to the passenger and driver in a given ride this this this // bind the function for booking a ride // next: add bookRide() function The 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. bookRide() bookRide() { RNGooglePlacePicker.show( { (response.didCancel){ .log( ); } (response.error){ .log( , response.error); } { .setState({ : , destination: response }); pickup_data = { : .state.origin.name, : .state.location.latitude, : .state.location.longitude }; dropoff_data = { : response.name, : response.latitude, : response.longitude }; .available_drivers_channel.trigger( , { : .username, : pickup_data, : dropoff_data }); } }); } ( ) => response if console 'User cancelled GooglePlacePicker' else if console 'GooglePlacePicker Error: ' else this is_searching true // show the loader // update the destination, this is used in the UI to display the name of the place // the pickup location / origin let name this latitude this longitude this // the dropoff / destination let name latitude longitude // send a ride request to all drivers this 'client-driver-request' username this pickup dropoff // next: add _setCurrentLocation() function The function gets the passenger’s current location. Note that here we’re using as opposed to which we used in the driver app earlier. The only difference between the two is that only gets the location once. _setCurrentLocation() getCurrentPosition() watchPosition() getCurrentPosition() _setCurrentLocation() { navigator.geolocation.getCurrentPosition( { region = regionFrom( position.coords.latitude, position.coords.longitude, position.coords.accuracy ); Geocoder.getFromLatLng(position.coords.latitude, position.coords.longitude).then( { address_component = json.results[ ].address_components[ ]; .setState({ : { name: address_component.long_name, latitude: position.coords.latitude, : position.coords.longitude }, : region, destination: , : , : , : }); }, (error) => { .log( , error); } ); }, (error) => .setState({ : error.message }), { : , : , : }, ); } ( ) => position var // get the name of the place by supplying the coordinates ( ) => json var 0 0 this origin // the passenger's current location // the name of the place longitude location // location to be used for the Map null has_ride false has_ridden false driver null console 'err geocoding: ' this error enableHighAccuracy false timeout 10000 maximumAge 3000 // 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. componentDidMount() { ._setCurrentLocation(); pusher = Pusher( , { : , : , : }); .available_drivers_channel = pusher.subscribe( ); .user_ride_channel = pusher.subscribe( + .username); } this // set current location of the passenger // connect to the auth server var new 'YOUR PUSHER API KEY' authEndpoint 'YOUR AUTH SERVER ENDPOINT' cluster 'YOUR PUSHER CLUSTER' encrypted true // subscribe to the available drivers channel this 'private-available-drivers' // subscribe to the passenger's private channel this 'private-ride-' this // next: add code for listening to handshake responses 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. .user_ride_channel.bind( , (data) => { passenger_response = ; (! .state.has_ride){ passenger_response = ; } .user_ride_channel.trigger( , { : passenger_response }); }); this 'client-driver-response' let 'no' if this // passenger is still looking for a ride 'yes' // passenger responds to driver's response this 'client-driver-response' response // next: add listener for when a driver is found The driver sends their information by triggering the event. As you have seen in the driver app earlier, this contains the name of the driver as well as their current location. client-found-driver .user_ride_channel.bind( , (data) => { region = regionFrom( data.location.latitude, data.location.longitude, data.location.accuracy ); .setState({ : , is_searching: , location: region, driver: { latitude: data.location.latitude, : data.location.longitude, : data.location.accuracy } }); Alert.alert( , + data.driver.name + + data.location.name, [ { : }, ], { : } ); }); this 'client-found-driver' // the driver's location info let this has_ride true // passenger has already a ride false // stop the loading UI from spinning // display the driver's location in the map // the driver location details longitude accuracy // alert the passenger that a driver was found "Orayt!" "We found you a driver. \nName: " "\nCurrent location: " text 'Sweet!' cancelable false // 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: .user_ride_channel.bind( , (data) => { region = regionFrom( data.latitude, data.longitude, data.accuracy ); .setState({ : region, driver: { : data.latitude, : data.longitude } }); }); this 'client-driver-location' let // update the Map to display the current location of the driver this location // the driver's location latitude longitude 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 ( ) and also when they’re already near the drop-off location ( ). near_pickup near_dropoff .user_ride_channel.bind( , (data) => { (data.type == ){ .setState({ : }); } (data.type == ){ ._setCurrentLocation(); } Alert.alert( data.title, data.msg, [ { : }, ], { : } ); }); this 'client-driver-message' if 'near_pickup' // the driver is very near the pickup location // remove passenger marker since we assume that the passenger has rode the vehicle at this point this has_ridden true if 'near_dropoff' // they're near the dropoff location this // assume that the ride is over, so reset the UI to the current location of the passenger // display the message sent from the driver app text 'Aye sir!' cancelable false // 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 ( ) 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. origin render() { ( <Spinner visible={this.state.is_searching} textContent={"Looking for drivers..."} textStyle={{color: '#FFF'}} /> <View style={styles.header}> <Text style={styles.header_text}>GrabClone</Text> </View> { !this.state.has_ride && <View style={styles.form_container}> <Button onPress={this.bookRide} title="Book a Ride" color="#103D50" /> </View> } <View style={styles.map_container}> { this.state.origin && this.state.destination && <View style={styles.origin_destination}> <Text style={styles.label}>Origin: </Text> <Text style={styles.text}>{this.state.origin.name}</Text> <Text style={styles.label}>Destination: </Text> <Text style={styles.text}>{this.state.destination.name}</Text> </View> } { this.state.location && <MapView style={styles.map} region={this.state.location} > { this.state.origin && !this.state.has_ridden && <MapView.Marker coordinate={{ latitude: this.state.origin.latitude, longitude: this.state.origin.longitude}} title={"You're here"} /> } { this.state.driver && <MapView.Marker coordinate={{ latitude: this.state.driver.latitude, longitude: this.state.driver.longitude}} title={"Your driver is here"} pinColor={"#4CDB00"} /> } </MapView> } </View> </View> ); } return < = > View style {styles.container} Lastly, add the styles: styles = StyleSheet.create({ : { ...StyleSheet.absoluteFillObject, : }, : { : , : , : }, : { : , : , }, : { : , : , : }, : { : , : }, : { : }, : { : , : , }, : { : }, : { : }, }); const container justifyContent 'flex-end' form_container flex 1 justifyContent 'center' padding 20 header padding 20 backgroundColor '#333' header_text color '#FFF' fontSize 20 fontWeight 'bold' origin_destination alignItems 'center' padding 10 label fontSize 18 text fontSize 18 fontWeight 'bold' map_container flex 9 map flex 1 Now you’re ready to run the app. As I mentioned in the section earlier, you’re going to optionally need two machines, one for running each of the app. This will allow you to enable logging ( ) 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. Prerequisites console.log 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 it's 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 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. App Flow If you’re using two machines, then you can simply run on both. One should be connected to your device and the other should have the Genymotion emulator open. react-native run-android Conclusion That’s it! In this tutorial you’ve learned how to make use of Pusher to create a ride-booking app. As you have seen, the app that you’ve built is pretty bare-bones. We’ve only stick to building the most important parts of a ride-booking app. If you want you can add more features to the app and maybe use it on your own projects. You can find the source code used in this app on its . GitHub repo This tutorial was originally published on the Pusher Tutorial Hub .