Read Part I here This is the second part of a two-part series on creating a carpooling app with React Native. This is the part where we will be creating the actual app. I’ll be showing you how to set up the app so you can run it on an Android emulator (Genymotion) or an iOS device. This tutorial has the same prerequisites as the first part. The following needs to be set up on your machine: React Native development environment Docker and Docker Compose Git Additionally, you should have already a running server instance which is exposed to the internet via ngrok. Be sure to check out the first part if you haven’t set up any of these yet. To effectively follow this tutorial, you should have a good grasp of the following React concepts: props refs state component lifecycle As for building the app with React Native, knowing how to do the following will be helpful: How to use primitive React Native components such as the or . View Text How to add styles to the components. How to create your own components. What we'll be building The complete details on what we’ll be building are available in the first part of the series. As a refresher, we’ll be building a carpooling app. This allows the user to share the vehicle they’re currently riding in so someone else can hop in the same vehicle. The app is responsible for: Matching the users so that only the users who are going the same route can share a ride with each other. After two users are matched, the app provides realtime tracking on where each other currently are. For the rest of the tutorial, I’ll be referring to the user who is sharing the ride as the “rider”. While the user who is searching for a ride as the “hiker”. Installing the dependencies Start by generating a new React Native project: react-native init Ridesharer This will create a directory. This will serve as the root directory that we’ll be using for the rest of the tutorial. Ridesharer The app relies on the following libraries to implement specific features: - for making requests to the server. Although React Native already comes with , axios gives us a simpler API to work with. axios fetch - the official Pusher JavaScript library. This allows us connect to a Pusher app and send realtime data. pusher-js - for converting latitude and longitude pairs to the actual name of the place. react-native-geocoding - for searching the user’s destination. react-native-google-places-autocomplete - for showing a map inside the app. This is also used for showing markers on where the users are and their destinations. react-native-maps - for showing the route from the user’s origin to their destination. react-native-maps-directions - for using icons inside the app. react-native-vector-icons - for easily implementing navigation between screens. react-navigation To ensure that we’re both using the same package versions, open the file and update the with the following: package.json dependencies : { : , : , : , : , : , : , : , : , : , : , : }, "dependencies" "axios" "0.18.0" "prop-types" "15.6.1" "pusher-js" "4.2.2" "react" "16.3.1" "react-native" "0.55.4" "react-native-geocoding" "0.3.0" "react-native-google-places-autocomplete" "1.3.6" "react-native-maps" "0.20.1" "react-native-maps-directions" "1.6.0" "react-native-vector-icons" "4.6.0" "react-navigation" "2.0.1" Once that’s done, save the file and execute . npm install Setting up the dependencies Now that you’ve installed all the dependencies, there’s one more thing you have to do before you can start coding the app. Additional setup is required for the following dependencies: react-native-vector-icons react-native-maps Instructions on how to set up the dependencies are available on the GitHub repos for each library. Here are the links to the setup instructions to the specific version we’re using: react-native-vector-icons v4.6.0 react-native-maps v0.20.1 Note that if you’re reading this sometime in the future, you’ll probably have to install the latest package versions and follow their latest installation instructions. Building the app Now we’re ready to build the app. Navigate inside the directory as that’s going to be our working directory. Ridesharer Note that anytime you feel confused on where to add a specific code, you can always visit the and view the file. GitHub repo Index Open the file and make sure you’re registering the same name that you used when you generated the project. In this case, it should be : index.js Ridesharer import { AppRegistry } from ; import App from ; AppRegistry.registerComponent( , () => App); // Ridesharer/index.js 'react-native' './App' 'Ridesharer' Root component Create a file. This will serve as the Root component of the app. This is where we set up the navigation so we include the two pages of the app: Home and Map. We will be creating these pages later: Root.js React ; { StackNavigator } ; HomePage ; MapPage ; RootStack = StackNavigator( { : { : HomePage }, : { : MapPage } }, { : , } ); RootStack; // Ridesharer/Root.js import from 'react' import from 'react-navigation' import from './app/screens/Home' import from './app/screens/Map' const Home screen Map screen initialRouteName 'Home' // set the home page as the default page export default In the above code, we’re using the , one of the navigators that comes with the React Navigation library. This allows us to push and pop pages to and from a stack. Navigating to a page means pushing it in front of the stack, going back means popping the page that’s currently in front of the stack. StackNavigator App component Open the file and render the App component: App.js React, { Component } ; { StyleSheet, View } ; Root ; { render() { ( <Root /> ); } } styles = StyleSheet.create({ : { : , : } }); // Ridesharer/App.js import from 'react' import from 'react-native' import from './Root' export default class App extends Component return < = > View style {styles.container} </ > View const container flex 1 backgroundColor '#fff' Tapper component The component is simply a button created for convenience. We can’t really apply a custom style to the built-in React Native component so we’re creating this one. This component wraps the component in a in which the styles are applied: Tapper Button Button View React ; { View, Button } ; styles ; Tapper = { ( <Button onPress={props.onPress} title={props.title} color={props.color} /> </View> // Ridesharer/app/components/Tapper/Tapper.js import from 'react' import from 'react-native' import from './styles' const ( ) => props return < = > View style {styles.button_container} ); } export default Tapper; Here’s the style declaration: { StyleSheet } ; StyleSheet.create({ : { : }, }); // Ridesharer/app/components/Tapper/styles.js import from 'react-native' export default button_container margin 10 Lastly, we export it using an file so that we can simply refer to the component as without including the file in the import statement later on: index.js Tapper Tapper.js Tapper ; Tapper; // Ridesharer/app/components/Tapper/index.js import from './Tapper' export default If you don’t want to create a separate component, you can always use the and components. Those two allow you to add a custom style. TouchableOpacity TouchableHighlight Home page The page is the default page the user sees when they open the app. Home Start by including all the React Native packages that we need: React, { Component } ; { View, Text, StyleSheet, TextInput, Alert, ActivityIndicator, PermissionsAndroid, KeyboardAvoidingView } ; // Ridesharer/app/screens/Home.js import from 'react' import from 'react-native' Among the packages above, only these three warrants an explanation: - for asking permissions to use the device’s Geolocation feature on Android. PermissionsAndroid - for automatically adjusting the View when the on-screen keyboard pops out. This allows the user to see what they’re inputting while the keyboard is open. Most of the time, especially on devices with small screen, the input is hidden when the keyboard is open. KeyboardAvoidingView Next, include the third-party packages we installed earlier: axios ; Icon ; Tapper ; import from 'axios' import from 'react-native-vector-icons/FontAwesome' import from '../components/Tapper' Add your ngrok URL (this was created in the first part of the series): base_url = ; const 'YOUR NGROK URL' Declare the function that will ask for the permission and then call it: Geolocation { { granted = PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, { : , : } ); (granted === PermissionsAndroid.RESULTS.GRANTED){ .log( ) } { .log( ) } } (err){ .warn(err) } } requestGeolocationPermission(); async ( ) function requestGeolocationPermission try const await 'title' 'Ridesharer Geolocation Permission' 'message' 'Ridesharer needs access to your current location so you can share or search for a ride' if console "You can use the geolocation" else console "Geolocation permission denied" catch console Hide the header. The page doesn’t need it: Home { navigationOptions = { : , }; } export default class Home extends Component static header null Set the default state for the loader (for controlling the visibility of the ) and username: ActivityIndicator state = { : , : } is_loading false username '' Render the Home page. In this page we have: An input that asks for the user’s name A button for sharing a ride A button for hitching a ride Note that we’re using the as a wrapper. This way, everything inside it will adjust accordingly when the on-screen keyboard becomes visible: KeyboardAvoidingView render() { ( <View style={styles.jumbo_container}> <Icon name="question-circle" size={35} color="#464646" /> <Text style={styles.jumbo_text}>What do you want to do?</Text> </View> <View> <TextInput placeholder="Enter your username" style={styles.text_field} onChangeText={(username) => this.setState({username})} value={this.state.username} clearButtonMode={"always"} returnKeyType={"done"} /> <ActivityIndicator size="small" color="#007ff5" style={{marginTop: 10}} animating={this.state.is_loading} /> </View> <View style={styles.close_container}> <Tapper title="Share a Ride" color="#007ff5" onPress={() => { this.enterUser('share'); }} /> <Tapper title="Hitch a Ride" color="#00bcf5" onPress={() => { this.enterUser('hike'); }} /> </View> </KeyboardAvoidingView> ); } return < = = > KeyboardAvoidingView style {styles.container} behavior "padding" enabled When either of the buttons is pressed, the function below gets executed. All it does is create the user if they don’t already exist: enterUser = { ( .state.username){ .setState({ : }); axios.post( , { : .state.username }) .then( { (response.data == ){ .setState({ : }); .props.navigation.navigate( , { : action, : .state.username }); } }); } { Alert.alert( , ); } } ( ) => action if this // user should enter a username before they can enter this is_loading true // make a POST request to the server for creating the user ` /save-user.php` ${base_url} username this // the username entered in the text field ( ) => response if 'ok' // hide the ActivityIndicator this is_loading false // navigate to the Map page, submitting the user's action (ride or hike) and their username as a navigation param (so it becomes available on the Map page) this 'Map' action username this else 'Username required' 'Please enter a username' Add the styles for the Home page: styles = StyleSheet.create({ : { : , : , : }, : { : , : }, : { : , : , : , : }, : { : , : , : , : , : , : } }); const container flex 1 alignItems 'center' justifyContent 'space-around' jumbo_container padding 50 alignItems 'center' jumbo_text marginTop 20 textAlign 'center' fontSize 25 fontWeight 'bold' text_field width 200 height 50 padding 10 backgroundColor '#FFF' borderColor 'gray' borderWidth 1 Map page The Map page contains the main meat of the app. This allows the user to share or search for a ride. The tracking of location is implemented via Google Maps, Pusher Channels, and React Native’s Geolocation feature. Start by including all the React Native packages that we need: React, { Component } ; { View, Text, StyleSheet, Alert, Dimensions, ActivityIndicator } ; // Ridesharer/app/screens/Map.js import from 'react' import from 'react-native' Next, include the packages that we installed earlier: { GooglePlacesAutocomplete } ; MapView, { Marker, Callout } ; MapViewDirections ; Icon ; Pusher ; Geocoder ; axios ; import from 'react-native-google-places-autocomplete' import from 'react-native-maps' import from 'react-native-maps-directions' import from 'react-native-vector-icons/FontAwesome' import from 'pusher-js/react-native' import from 'react-native-geocoding' import from 'axios' Include the location library. We will be creating this later, but for now, know that these functions are used to center the map correctly ( ) and getting the difference of two coordinates in meters ( ): regionFrom() getLatLonDiffInMeters() { regionFrom, getLatLonDiffInMeters } ; Tapper ; import from '../lib/location' import from '../components/Tapper' Initialize your API keys and ngrok base URL: google_api_key = ; base_url = ; pusher_app_key = ; pusher_app_cluster = ; Geocoder.init(google_api_key); const 'YOUR GOOGLE PROJECT API KEY' const 'YOUR NGROK BASE URL' const 'YOUR PUSHER APP KEY' const 'YOUR PUSHER APP CLUSTER' // initialize the geocoder Next, also declare the timeouts for searching and sharing a ride. We will be using these values later to reset the app’s UI if it couldn’t match two users within these timeouts: search_timeout = * * ; share_timeout = * * ; const 1000 60 10 // 10 minutes const 1000 60 5 // 5 minutes Setup a default region that the map will display: default_region = { : , : , : , : , }; const latitude 37.78825 longitude -122.4324 latitudeDelta 0.0922 longitudeDelta 0.0421 Get the device width. We will be using this later to set the width of the auto-complete text field for searching places: device_width = Dimensions.get( ).width; var 'window' Next, create the component and set the . Unlike the page earlier, we need to set a few options for the navigation. This includes the header title and the styles applied to it. Putting these navigation options will automatically add a back button to the header to allow the user to go back to the page: Map navigationOptions Home Home { navigationOptions = ({ : , : { : }, : { : } }); } export default class Map extends Component static ( ) => {navigation} headerTitle 'Map' headerStyle backgroundColor '#007ff5' headerTitleStyle color '#FFF' // next: add the code for initializing the state Next, initialize the state: state = { : , end_location: , region: default_region, : , to: , rider_location: , hiker_location: , is_loading: , has_journey: } start_location null // the coordinates (latitude and longitude values) of the user's origin null // the coordinates of the user's destination // the region displayed in the map from '' // the name of the place where the user is from (origin) '' // the name of the place where the user is going (destination) null // the coordinates of the rider's current location null // the coordinates of the hiker's origin false // for controlling the visibility of the ActivityIndicator false // whether the rider has accepted a hiker's request or a hiker's request has been accepted by a rider // next: add the constructor Next, add the constructor: (props) { (props); .from_region = ; .watchId = ; .pusher = ; .user_channel = ; .journey_id = ; .riders_channel = []; .users_channel = ; .hiker = } constructor super this null this null // unique ID for the geolocation watcher. Storing it in a variable allows us to stop it at a later time (for example: when the user is done using the app) this null // variable for storing the Pusher instance this null // the Pusher channel for the current user this null // the hiker's route ID this // if current user is a hiker, the value of this will be the riders channel this null // the current user's channel this null // for storing the hiker's origin coordinates; primarily used for getting the distance between the rider and the hiker Once the component is mounted, you want to get the that was passed from the Home page earlier. This is used later on as the unique key for identifying each user that connects to Pusher Channels: username username componentDidMount() { { navigation } = .props; username = navigation.getParam( ); .pusher = Pusher(pusher_app_key, { : , : pusher_app_cluster, : }); } const this const 'username' this new authEndpoint ` /pusher-auth.php` ${base_url} cluster encrypted true // next: add the code for subscribing to the current user's own channel Next, add the code for subscribing to the current user's own channel. This allows the user to send and receive data in realtime through this channel. In the hiker’s case, they use it to make a request to the matching rider. In the rider’s case, they use it to receive requests coming from hikers as well as sending an acceptance and their current location to the hiker: .users_channel = .pusher.subscribe( ); this this `private-user- ` ${username} // note that the private-* is required when using private channels When a rider receives a request, the code below is executed. This alerts the rider that someone wants to ride with them. They can either accept or decline it: .users_channel.bind( , (hiker) => { Alert.alert( , , [ { : , : { }, : }, { : , : { .acceptRide(hiker); } }, ], { : } ); }); this 'client-rider-request' ` wants to ride with you` ${hiker.username} `Pickup: \nDrop off: ` ${hiker.origin} ${hiker.dest} text "Decline" onPress => () // do nothing style "cancel" text "Accept" onPress => () this cancelable false // no cancel button // next: add code for getting the user's origin Note that in the code above, we’re not really handling declines. This is to keep the focus on the key feature of the app. Next, get the user’s current location via the Geolocation API. At this point, we can already use the API without problems (unless the user didn’t approve the permission). We’ll just focus our attention on the “happy path” to keep things simple so we’ll assume that the user approved the permission request: navigator.geolocation.getCurrentPosition( { region = regionFrom( position.coords.latitude, position.coords.longitude, position.coords.accuracy ); Geocoder.from({ : position.coords.latitude, : position.coords.longitude }) .then( { .from_region = region; .setState({ : { : position.coords.latitude, : position.coords.longitude }, : region, : response.results[ ].formatted_address }); }); } ); ( ) => position // get the region (this return the latitude and longitude delta values to be used by React Native Maps) var // convert the coordinates to the descriptive name of the place latitude longitude ( ) => response // the response object is the same as what's returned in the HTTP API: https://developers.google.com/maps/documentation/geocoding/intro this // for storing the region in case the user presses the "reset" button // update the state to indicate the user's origin on the map (using a marker) this start_location latitude longitude region // the region displayed on the map from 0 // the descriptive name of the place Next, add the function. This function is executed when the rider accepts a hiker’s ride request: acceptRide() acceptRide = { username = .props.navigation.getParam( ); rider_data = { : username, : .state.from, dest: .state.to, coords: .state.start_location }; .users_channel.trigger( , rider_data); axios.post( , { : username }) .then( { .log(response.data); }) .catch( { .log( , err); }); .hiker = hiker; .setState({ : , : , : hiker.origin_coords }); } ( ) => hiker const this 'username' let username origin this // descriptive name of the rider's origin this // descriptive name of the rider's destination this // the rider's origin coordinates this 'client-rider-accepted' // inform hiker that the rider accepted their request; send along the rider's info // make a request to delete the route so other hikers can no longer search for it (remember the 1:1 ratio for a rider to hiker?) ` /delete-route.php` ${base_url} username ( ) => response console ( ) => err console 'error excluding rider: ' this // store the hiker's info // update the state to stop the loading animation and show the hiker's location this is_loading false has_journey true hiker_location Next, add the function for rendering the UI: render() { { navigation } = .props; action = navigation.getParam( ); username = navigation.getParam( ); action_button_label = (action == ) ? : ; } const this // get the navigation params passed from the Home page earlier const 'action' // action is either "ride" or "hike" const 'username' let 'share' 'Share Ride' 'Search Ride' // next: add code for rendering the UI The map UI contains the following: component for rendering the map. Inside it are the following: Marker component for showing the origin and destination of the user, as well as for showing the location of the rider (if the user is a hiker), or the hiker (if the user is a rider). It also contains the component for showing the route from the origin to the destination of the current user. MapView MapViewDirections component for rendering an auto-complete text field for searching and selecting a destination. GooglePlacesAutocomplete for showing a loading animation while the rider waits for someone to request a ride, or when the hiker waits for the app to find a matching rider. ActivityIndicator component for sharing a ride or searching a ride. Tapper component for resetting the selection (auto-complete text field and marker). Tapper ( <MapView style={styles.map} region={this.state.region} zoomEnabled={true} zoomControlEnabled={true} > { this.state.start_location && <Marker coordinate={this.state.start_location}> <Callout> <Text>You are here</Text> </Callout> </Marker> } { this.state.end_location && <Marker pinColor="#4196ea" coordinate={this.state.end_location} draggable={true} onDragEnd={this.tweakDestination} /> } { this.state.rider_location && <Marker pinColor="#25a25a" coordinate={this.state.rider_location} > <Callout> <Text>Rider is here</Text> </Callout> </Marker> } { this.state.hiker_location && <Marker pinColor="#25a25a" coordinate={this.state.hiker_location} > <Callout> <Text>Hiker is here</Text> </Callout> </Marker> } { this.state.start_location && this.state.end_location && <MapViewDirections origin={{ 'latitude': this.state.start_location.latitude, 'longitude': this.state.start_location.longitude }} destination={{ 'latitude': this.state.end_location.latitude, 'longitude': this.state.end_location.longitude }} strokeWidth={5} strokeColor={"#2d8cea"} apikey={google_api_key} /> } </MapView> <View style={styles.search_field_container}> <GooglePlacesAutocomplete ref="endlocation" placeholder='Where do you want to go?' minLength={5} returnKeyType={'search'} listViewDisplayed='auto' fetchDetails={true} onPress={this.selectDestination} query={{ key: google_api_key, language: 'en', }} styles={{ textInputContainer: { width: '100%', backgroundColor: '#FFF' }, listView: { backgroundColor: '#FFF' } }} debounce={200} /> </View> <ActivityIndicator size="small" color="#007ff5" style={{marginBottom: 10}} animating={this.state.is_loading} /> { !this.state.is_loading && !this.state.has_journey && <View style={styles.input_container}> <Tapper title={action_button_label} color={"#007ff5"} onPress={() => { this.onPressActionButton(); }} /> <Tapper title={"Reset"} color={"#555"} onPress={this.resetSelection} /> </View> } </View> ); return < = > View style {styles.container} The code above should be pretty self-explanatory. If you’re unsure what a specific prop does, how the component works, or what children is it expecting, you can always check the Github repo of the package we’re using. Next, let’s move on to the functions used in the UI. The is executed when the reset button is pressed by the user. This empties the auto-complete text field for searching for places, it also updates the state so the UI reverts back to its previous state before the destination was selected. This effectively removes the marker showing the user’s destination, as well as the route going to it: resetSelection() resetSelection = { .refs.endlocation.setAddressText( ); .setState({ : , : .from_region, : }); } => () this '' this end_location null region this to '' The function is executed when the user drops the destination marker somewhere else: tweakDestination() tweakDestination = { Geocoder.from({ : evt.nativeEvent.coordinate.latitude, : evt.nativeEvent.coordinate.longitude }) .then( { .setState({ : response.results[ ].formatted_address }); }); .setState({ : evt.nativeEvent.coordinate }); } => () // get the name of the place latitude longitude ( ) => response this to 0 this end_location The function is executed when the user selects their destination. This function will update the state so it shows the user’s destination in the map: selectDestination() selectDestination = { latDelta = (details.geometry.viewport.northeast.lat) - (details.geometry.viewport.southwest.lat) lngDelta = (details.geometry.viewport.northeast.lng) - (details.geometry.viewport.southwest.lng) region = { : details.geometry.location.lat, : details.geometry.location.lng, : latDelta, : lngDelta }; .setState({ : { : details.geometry.location.lat, : details.geometry.location.lng, }, : region, : .refs.endlocation.getAddressText() }); } ( ) => data, details = null const Number Number const Number Number let latitude longitude latitudeDelta longitudeDelta this end_location latitude longitude region to this // get the full address of the user's destination When the user presses the or button, the function is executed. This executes either the function or the function depending on the action selected from the Home page earlier: Share a Ride Search a Ride onPressActionButton() shareRide() hikeRide() onPressActionButton = { action = .props.navigation.getParam( ); username = .props.navigation.getParam( ); .setState({ : }); (action == ){ .shareRide(username); } (action == ){ .hikeRide(username); } } => () const this 'action' const this 'username' this is_loading true if 'share' this else if 'hike' this The function is executed when a rider shares their ride after selecting a destination. This makes a request to the server to save the route. The response contains the unique ID assigned to the rider’s route. This ID is assigned as the value of . This will be used later to: shareRide() this.journey_id Make a request to the server to update the route record stored in the Elasticsearch index. Know when to start doing something with the current location data. This is because the current position begins to be watched right after the user presses on the button as you’ll see on the code block after this: Share a Ride shareRide = { axios.post( , { : username, : .state.from, : .state.to, : .state.start_location, : .state.end_location }) .then( { .journey_id = response.data.id; Alert.alert( , ); }) .catch( { .log( , error); }); } ( ) => username ` /save-route.php` ${base_url} username from this to this start_location this end_location this ( ) => response this 'Ride was shared!' 'Wait until someone makes a request.' ( ) => error console 'error occurred while saving route: ' // next: add code for watching the rider's current location Next, start watching the user’s current location. Note that we won’t actually do anything with the location data unless the rider has already shared their ride and that they have already approved a hiker to ride with them. Once both conditions are met, we make a request to the server to update the previously saved route with the rider’s current location. This way, when a hiker searches for a ride, the results will be biased based on the rider’s current location and not their origin: .watchId = navigator.geolocation.watchPosition( { latitude = position.coords.latitude; longitude = position.coords.longitude; accuracy = position.coords.accuracy; ( .journey_id && .hiker){ axios.post( , { : .journey_id, : latitude, : longitude }) .then( { .log(response); }); } }, (error) => { .log( , error); }, { : , timeout: , maximumAge: , distanceFilter: } ); this ( ) => position let let let if this this // needs to have a destination and a hiker // update the route with the rider's current location ` /update-route.php` ${base_url} id this lat lon ( ) => response console // next: add code for sending rider's current location to the hiker console 'error occured while watching position: ' enableHighAccuracy true // get more accurate location 20000 // timeout after 20 seconds of not being able to get location 2000 // location has to be atleast 2 seconds old for it to be relevant 10 // allow up to 10-meter difference from the previous location before executing the callback function again // last: add code for resetting the UI after 5 minutes of sharing a ride Next, we send a event to the rider’s own channel. Later, we’ll have the hiker subscribe to the rider’s channel (the one they matched with) so that they’ll receive the location updates: client-rider-location location_data = { : username, : latitude, : longitude, : accuracy }; .users_channel.trigger( , location_data); .setState({ : regionFrom(latitude, longitude, accuracy), : { : latitude, : longitude } }); let username lat lon accy this 'client-rider-locationchange' // note: client-* is required when sending client events through Pusher // update the state so that the rider’s current location is displayed on the map and indicated with a marker this region start_location latitude longitude // next: add code for updating the app based on how near the rider and hiker are from each other Next, we need to get the difference (in meters) between the rider’s coordinates and the hiker’s origin: diff_in_meters = getLatLonDiffInMeters(latitude, longitude, .hiker.origin_coords.latitude, .hiker.origin_coords.longitude); (diff_in_meters <= ){ .resetUI(); } (diff_in_meters <= ){ Alert.alert( , ); } let this this if 20 this else if 50 'Hiker is near' 'Hiker is around 50 meters from your current location' Next, add the code for resetting the UI after five minutes without anyone requesting to share a ride with the rider: setTimeout( { .resetUI(); }, share_timeout); => () this Here’s the code for resetting the UI: resetUI = { .from_region = ; .watchId = ; .pusher = ; .user_channel = ; .journey_id = ; .riders_channel = []; .users_channel = ; .hiker = ; .setState({ : , : , : default_region, : , : , : , : , : , : }); .props.navigation.goBack(); Alert.alert( , ); } => () this null this null this null this null this null this this null this null this start_location null end_location null region from '' to '' rider_location null hiker_location null is_loading false has_journey false this // go back to the Home page 'Awesome!' 'Thanks for using the app!' Now let’s move on to the hiker’s side of things. When the hiker presses the button, the function is executed. This function is executed every five seconds until it finds a rider which matches the hiker’s route. If a rider cannot be found within ten minutes, the function stops. Once the server returns a suitable rider, it responds with the rider’s information (username, origin, destination, coordinates). This is then used to subscribe to the rider’s channel so the hiker can request for a ride and receive location updates. Note that this is done automatically, so the hiker doesn’t have control over who they share a ride with: Search a Ride hikeRide() hikeRide = { interval = setInterval( { axios.post( , { : .state.start_location, : .state.end_location }) .then( { (response.data){ clearInterval(interval); rider = response.data; .riders_channel = .pusher.subscribe( ); .riders_channel.bind( , () => { .riders_channel.trigger( , { : username, origin: .state.from, dest: .state.to, origin_coords: .state.start_location }); }); } }) .catch( { .log( , error); }); }, ); setTimeout( { clearInterval(interval); .resetUI(); }, ten_minutes); } ( ) => username var => () // make a request to the server to get riders that matches the hiker's route ` /search-routes.php` ${base_url} origin this dest this ( ) => response if // assumes the rider will accept the request let // the rider's info // subscribe to the rider's channel so the hiker can make a request and receive updates from the rider this this `private-user- ` ${rider.username} this 'pusher:subscription_succeeded' // when subscription succeeds, make a request to the rider to share the ride with them this 'client-rider-request' username // username of the hiker this // descriptive name of the hiker's origin this // descriptive name of the hiker's destination this // coordinates of the hiker's origin // next: add code for listening for when the rider accepts their request ( ) => error console 'error occurred while searching routes: ' 5000 => () this Once the rider accepts the ride request, the function below is executed: .riders_channel.bind( , (rider_data) => { Alert.alert( , ); .setState({ : , : , : rider_data.coords }); }); this 'client-rider-accepted' ` accepted your request` ${rider_data.username} `You will now receive updates of their current location` // update the map to show the rider's origin this is_loading false has_journey true rider_location // next: add code for subscribing to the rider's location change As you’ve seen earlier, when the rider’s location changes, it triggers an event called . Any user who is subscribed to the rider’s channel and is listening for that event will get the location data in realtime: client-rider-location-change .riders_channel.bind( , (data) => { .setState({ : regionFrom(data.lat, data.lon, data.accy), : { : data.lat, : data.lon } }); hikers_origin = .state.start_location; diff_in_meters = getLatLonDiffInMeters(data.lat, data.lon, hikers_origin.latitude, hikers_origin.longitude); (diff_in_meters <= ){ .resetUI(); } (diff_in_meters <= ){ Alert.alert( , ); } }); this 'client-rider-locationchange' // update the map with the rider's current location this region rider_location latitude longitude let this let if 20 this else if 50 'Rider is near' 'Rider is around 50 meters from your location' Add the styles for the Map page: styles = StyleSheet.create({ : { : , : , : , : , : , : , : , }, : { : , : , : , : , : , }, : { : , : device_width, : , : }, : { : , : , : , : } }); const container position 'absolute' top 0 left 0 right 0 bottom 0 justifyContent 'flex-end' alignItems 'center' map position 'absolute' top 0 left 0 right 0 bottom 0 search_field_container height 150 width position 'absolute' top 10 input_container alignSelf 'center' backgroundColor '#FFF' opacity 0.80 marginBottom 25 Location library Here’s the code for getting the latitude and longitude delta values. As you have seen from the code earlier, this function is mainly used to get the region displayed on the map: { oneDegreeOfLongitudeInMeters = * ; circumference = ( / ) * ; latDelta = accuracy * ( / ( .cos(lat) * circumference)); lonDelta = (accuracy / oneDegreeOfLongitudeInMeters); { : lat, : lon, : .max( , latDelta), : .max( , lonDelta) }; } // Ridesharer/app/lib/location.js 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 And here’s the function for getting the difference (in meters) between two coordinates. This is mainly used for notifying the users when they’re already near each other, and to reset the app UI when they’re already very near each other: { 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 * ; } 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 The function used above converts the degrees value to radians: deg2rad() { deg * ( .PI/ ) } ( ) function deg2rad deg return Math 180 Running the app Before you can run the app on Android, you need to make sure you have the following Android SDK packages installed, you can find these under SDK Tools on the SDK manager: Google Play services Android Support Repository Google Repository If you’re going to test the app on Genymotion, you need to install Google Play services first. Since the app is using Google Maps, you need Google Play services for the feature to work. If you have version 2.10 or above, they provide an easy way to install it. Just click on on a running emulator instance and go through the installation wizard. After that, restart the device and you should be good to go: Open GAPPS To run the app on Android, execute the following command. This will run the app either on an opened emulator instance (for example: Genymotion) or an Android device (if you have connected one): react-native run-android If you’re having problems with getting the app to run on Android, be sure to check my article on Debugging common React Native issues on Android. For iOS, you just have to make sure you have the latest version of Xcode installed. Note that if you want to run the app on a device, you can only do it via Xcode by opening the . file. xcworkspace To run the app on an iOS device, select your device on Xcode and click the big play button. To run the app in the iOS simulator, you can also do it via Xcode using the method above. But if you want to run it from the terminal, you can execute the following command from the root directory of your project: react-native run-ios If you want to run the app on a specific simulator, you first have to list which devices are available: xcrun simctl list devicetypes This will return the list of devices: You can then copy the device name (for example: iPhone 5s) and specify it as a value for the option: --simulator react-native run-ios --simulator= "iPhone 5s" If you’re having problems with running the app on an iOS simulator or device, be sure to check my article on Debugging common React Native issues on iOS. Conclusion That’s it! In this series, you’ve learned how to create a carpooling app with React Native. Along the way, you also learned the following: How to use axios to make requests to the server. How to use React Native’s Geolocation feature. How to add Google Play Services to Genymotion. How to use Genymotion’s GPS emulation tool. How to use Pusher Channels. How to use Google’s Geocoding API. You can find all the codes used in this series on this . GitHub repo Originally published on the Pusher tutorial hub .