Create a food ordering app in React Native - Part 1: Making an order
You will need Node 11.10+, Yarn 1.17+, React Native CLI 2+ and React Native 0.61+ installed on your machine.
Food ordering apps such as Uber Eats and FoodPanda are popular these days as they allow you to conveniently order foods from your favorite local restaurant right from your phone.
In this tutorial, we’ll take a look at how to create a food ordering app in React Native. We will create the ordering app as well as the driver app.
Here’s a breakdown of what we will be discussing throughout the series:
- Part 1: Making an order
- Part 2: Adding the driver app and chat functionality
- Part 3: Adding push notifications
Prerequisites
Basic knowledge of React Native and Node.js is required to follow this tutorial.
We will use the following package versions:
- Node 11.10.1
- Yarn 1.17.3
- React Native CLI 2.0.1
- React Native 0.61.1
Be sure to use the versions indicated above if you encounter any issues getting the app to run.
You also need a Pusher Channels account and an ngrok account. We will use Channels to establish a connection between the customer and the driver, while ngrok is for exposing the server to the internet.
App overview
We will create a simplified version of a food ordering app. First, the user will be greeted with a food list. From here, they can click on any of the items to view the details:
Here’s what the details screen looks like. This is where they can select the quantity and add the item to the cart. Adding an existing item to the cart will result in incrementing the quantity of the item that’s already in the cart. Note that users can only order from one restaurant at a time:
Once the user is done adding items to their cart, they can click on the View Basket button in the header. This will navigate them to the order summary screen. This screen is where all the items they added to their cart is listed along with the amount they need to pay. This is also where they can change their delivery location:
Though Geolocation is used by default to determine the user’s location, if it isn’t accurate then the user can also pick their location:
Once the user is ready, they can click on the Place Order button to trigger the app to send a request to a driver.
Once a driver has accepted their request, the driver’s location is displayed in realtime on the map. The path from the driver to the restaurant and from the restaurant to the user is also indicated on the map:
You can find the app’s code on this GitHub repo. The completed code for this part of the series is on the food-ordering
branch.
Setting up Channels
Create a new Channels app instance if you haven’t already. Then under the App Settings tab, enable client events. This allows us to trigger events right from the app itself:
Setting up Google Maps
In order to use React Native Maps, you first need to set up the Google Maps Platform. Thankfully, this has been covered extensively in the official docs: Get Started with Google Maps Platform.
If you’re new to it, a highly recommend following the Quickstart. This is the fastest way to get up and running because it will automatically configure everything for you. All you need to do is pick the specific Google Maps products that you’re going to need. In this case, we’ll only need Maps and Maps Places. Selecting these will automatically enable the Android, iOS, and Web API of Google Maps and Places for you:
After that, you need to select a project. If you’re new to using any of the Google APIs, you will most likely have a project pre-created already. Just select that project or follow the instructions on how to create a new one:
After that, the final step is for you to setup billing.
Once that’s done, you should be able to view your API keys from the Google Cloud Platform console by clicking on the hamburger icon at the top left of the screen. Then select APIs & Services > Credentials. This will list out all the API keys that you can use for connecting to the Google Maps and Google Maps Places API. Here’s how it looks like:
Bootstrapping the app
The next step is for us to bootstrap the app. I’ve already prepared a starter
branch to make it easy for us to proceed with the important parts of the app. This branch contains the code for setting up the navigation as well as the code for the components and styles.
Clone the repo and switch to the starter
branch:
git clone https://github.com/anchetaWern/React-Native-Food-Delivery.git RNFoodDelivery
cd RNFoodDelivery
git checkout starter
After that, install all the dependencies. Note that this will only install the dependencies for this part of the series. We’ll install the dependencies for each part as we go:
yarn install
Here’s a what each of the packages are used for:
- axios - for making requests for the list of foods to the server.
- pusher-js - the JavaScript client library for Pusher. We use it as a realtime communication channel between the customer and the driver. This depends on
@react-native-community/netinfo
to determine the network status. - react-native-config - for reading config in the
.env
files. - react-navigation - for implementing navigation in the app. This depends on
react-navigation-stack
,react-native-gesture-handler
,react-native-reanimated
, andreact-native-gesture-handler
. - react-native-simple-stepper - used for rendering a stepper component for selecting the quantity for a specific order item.
- react-native-permissions - for requesting for permission to access geolocation data.
- react-native-geocoding - for converting coordinates to an actual place name.
- react-native-google-places - for rendering a place picker modal powered by Google Maps Places.
- react-native-geolocation-service - for getting the geolocation data.
- react-native-maps - for rendering maps and markers.
- react-native-maps-directions - for drawing a path between two coordinates.
Next, update the .env
file at the roof of the project directory with your Channels and Google Maps API credentials:
CHANNELS_APP_KEY="YOUR CHANNELS APP KEY"
CHANNELS_APP_CLUSTER="YOUR CHANNELS APP CLUSTER"
GOOGLE_API_KEY="YOUR GOOGLE API KEY"
NGROK_HTTPS_URL="YOUR NGROK HTTPS URL"
Next, update the android/settings.gradle
file to include the native files for the packages that we’re using. We’re not including all of them because most of the packages that we’re using doesn’t have native code and a few others already supports Autolinking:
rootProject.name = 'RNFoodDelivery'
// add these:
include ':react-native-permissions'
project(':react-native-permissions').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-permissions/android')
include ':react-native-config'
project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android')
include ':react-native-google-places'
project(':react-native-google-places').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-places/android')
Next, update the android/app/build.gradle
file:
apply plugin: "com.android.application"
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" // add this
Still on the same file, look for the dependencies
and add the following:
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.facebook.react:react-native:+" // From node_modules
// add these (for various dependencies)
implementation project(':react-native-config')
implementation project(':react-native-google-places')
implementation project(':react-native-permissions')
// add these (for react-navigation):
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'
}
Next, update the android/app/src/main/AndroidManifest.xml
file and include the permissions that we need. ACCESS_NETWORK_STATE
is used by Channels to determine if the user is currently connected to the internet. While ACCESS_FINE_LOCATION
is used for getting the user’s current location:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rnfooddelivery">
<uses-permission android:name="android.permission.INTERNET" />
<!-- add these -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
</manifest>
Still on the same file, under <application>
, add your Google API key config. This is required by React Native Maps in order to use Google Maps:
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR GOOGLE API KEY" />
</application>
Coding the ordering app
At this point, we’re now ready to start coding the app. As mentioned earlier, the navigation and styles have already been pre-coded. All we have to do now is add the code for the individual screens.
FoodList screen
First, we’ll go through the code for the FoodList screen. This screen displays the list of foods that are available for order from each of the restaurants that uses the app. Nothing too complex here. All we do is request the data from the server. As you’ll see later, the list of foods is also hard-coded.
Open the src/screens/FoodList.js
file and add the following. If you’ve used React Native for a while, you should feel right at home. Basically, we’re just creating a list using the FlatList
component and then filtering it by whatever the user has entered in the TextInput
. The navigationOptions
allows us to specify the settings for the navigation header for the current page. In this case, we include the title
and a Button
in the header for navigating to the OrderSummary screen. The React Navigation library takes care of these for us:
// src/screens/FoodList.js
import React, {Component} from 'react';
import {View, Text, Button, TextInput, FlatList, StyleSheet} from 'react-native';
import axios from 'axios';
import Config from 'react-native-config';
import NavHeaderRight from '../components/NavHeaderRight';
import ListCard from '../components/ListCard';
const BASE_URL = Config.NGROK_HTTPS_URL;
class FoodList extends Component {
static navigationOptions = ({navigation}) => {
return {
title: 'Hungry?',
headerRight: <NavHeaderRight />,
};
};
state = {
foods: [], // list of foods to be rendered on the screen
query: '',
};
async componentDidMount() {
// fetch the array of foods from the server
const foods_response = await axios.get('${BASE_URL}/foods');
this.setState({
foods: foods_response.data.foods,
});
}
render() {
const {foods, query} = this.state;
return (
<View style={styles.wrapper}>
<View style={styles.topWrapper}>
<View style={styles.textInputWrapper}>
<TextInput
style={styles.textInput}
onChangeText={this.onChangeQuery}
value={query}
placeholder={'What are you craving for?'}
/>
</View>
<View style={styles.buttonWrapper}>
<Button
onPress={() => this.filterList()}
title="Go"
color="#c53c3c"
/>
</View>
</View>
<FlatList
data={foods}
renderItem={this.renderFood}
contentContainerStyle={styles.list}
keyExtractor={item => item.id.toString()}
/>
</View>
);
}
onChangeQuery = text => {
this.setState({
query: text,
});
};
filterList = async () => {
// filter the list of foods by supplying a query
const {query} = this.state;
const foods_response = await axios.get(`${BASE_URL}/foods?query=${query}`);
this.setState({
foods: foods_response.data.foods,
query: '',
});
};
viewItem = item => {
// navigate to the FoodDetails screen
this.props.navigation.navigate('FoodDetails', {
item,
});
};
renderFood = ({item}) => {
return <ListCard item={item} viewItem={this.viewItem} />;
};
}
// <pre-coded styles here..>
export default FoodList;
FoodDetails screen
Next, let’s go through the code for the FoodDetails screen. This screen shows all the details for a specific food. It also allows the user to select the quantity to be ordered and add them to the cart. The PageCard
component is used for rendering the entirety of the screen. All we do is supply it with the necessary props. The most relevant function here is the function for adding the item to the cart. This implements the rule that the user can only order foods from a single restaurant for each order. But the addToCart()
method from this.context
is the one that actually adds it to the cart. We’ll walk through what this context is shortly. For now, know that this uses React’s Context API to create a global app context for storing data and function that we need throughout the app:
// src/screens/FoodDetails.js
import React, {Component} from 'react';
import {View, Button, Alert} from 'react-native';
import NavHeaderRight from '../components/NavHeaderRight';
import PageCard from '../components/PageCard';
import {AppContext} from '../../GlobalContext';
class FoodDetails extends Component {
static navigationOptions = ({navigation}) => {
return {
title: navigation.getParam('item').name.substr(0, 12) + '...',
headerRight: <NavHeaderRight />,
};
};
static contextType = AppContext; // set this.context to the global app context
state = {
qty: 1,
};
constructor(props) {
super(props);
const {navigation} = this.props;
this.item = navigation.getParam('item'); // get the item passed from the FoodList screen
}
qtyChanged = value => {
const nextValue = Number(value);
this.setState({qty: nextValue});
};
addToCart = (item, qty) => {
// prevent the user from adding items with different restaurant ids
const item_id = this.context.cart_items.findIndex(
el => el.restaurant.id !== item.restaurant.id,
);
if (item_id === -1) {
Alert.alert(
'Added to basket',
`${qty} ${item.name} was added to the basket.`,
);
this.context.addToCart(item, qty); // call addToCart method from global app context
} else {
Alert.alert(
'Cannot add to basket',
'You can only order from one restaurant for each order.',
);
}
};
render() {
const {qty} = this.state;
return (
<PageCard
item={this.item}
qty={qty}
qtyChanged={this.qtyChanged}
addToCart={this.addToCart}
/>
);
}
}
export default FoodDetails;
GlobalContext
As mentioned earlier, we’re using the React Context API to create a global context in which we store data and function that we need throughout the app. This allows us to avoid common problems when working with state such as prop drilling. All without having to use full-on state management libraries like Redux or MobX.
In this case, we need to make the cart items as well as the function for adding items available in the global app context. To do that, we create a context and export it. Then we create an AppContextProvider
component. This will serve as a wrapper for the higher-order component that we’re going to create shortly. Thus, it is where we initialize the global state and include the function for adding items to the cart. The addToCart()
method contains the logic that checks whether an item has already been added to the cart. If it is, then it will simply add the supplied quantity to the existing item:
// GlobalContext.js
import React from 'react';
import {withNavigation} from 'react-navigation';
export const AppContext = React.createContext({}); // create a context
export class AppContextProvider extends React.Component {
state = {
cart_items: [],
user_id: 'wernancheta',
user_name: 'Wern Ancheta',
};
constructor(props) {
super(props);
}
addToCart = (item, qty) => {
let found = this.state.cart_items.filter(el => el.id === item.id);
if (found.length == 0) {
this.setState(prevState => {
return {cart_items: prevState.cart_items.concat({...item, qty})};
});
} else {
this.setState(prevState => {
const other_items = prevState.cart_items.filter(
el => el.id !== item.id,
);
return {
cart_items: [...other_items, {...found[0], qty: found[0].qty + qty}],
};
});
}
};
// next: add render()
}
// last: export components
Here’s the render()
method. This is where we use the Context Provider component to allow consuming components to subscribe to context value changes. The value is specified via the value
prop. Using the Context Provider allows us to automatically re-render the consuming components everytime the value changes. In this case, we’re destructuring whatever is in the state and add the addToCart()
method:
render() {
return (
<AppContext.Provider
value={{
...this.state,
addToCart: this.addToCart,
}}>
{this.props.children}
</AppContext.Provider>
);
}
Once that’s done, we can now create the actual higher-order component and use the AppContextProvider
to wrap whatever component will be passed to it:
export const withAppContextProvider = ChildComponent => props => (
<AppContextProvider>
<ChildComponent {...props} />
</AppContextProvider>
);
If you’re having difficulty wrapping your head around higher-order components in React. Be sure to check out this article: How to develop your React superpowers with the HOC Pattern.
index.js
To use the higher-order component that we just created, open the index.js
file at the root of the project directory then wrap the main App
component with the withAppContextProvider
:
// index.js
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import {withAppContextProvider} from './GlobalContext'; // add this
AppRegistry.registerComponent(appName, () => withAppContextProvider(App)); // wrap App withAppContextProvider
Note that this doesn’t automatically provide us with whatever state is in the AppContextProvider
component. As you’ve seen in the src/screens/FoodDetails.js
file earlier, we had to include the AppContext
:
import {AppContext} from '../../GlobalContext';
Then inside the component class, we had to set the contextType
to the AppContext
:
class FoodDetails extends Component {
static contextType = AppContext;
// ...
}
This allowed us to access any of the values that were passed in the Context Provider component via this.context
:
this.context.cart_items;
this.context.addToCart(item, qty);
OrderSummary screen
Next, let’s proceed with the OrderSummary screen. This screen displays the items added to the cart and the payment breakdown. It also allows the user to change their delivery location.
Start by importing and initializing the packages we need:
// src/screens/OrderSummary.js
import React, {Component} from 'react';
import {
View,
Text,
Button,
TouchableOpacity,
FlatList,
StyleSheet,
} from 'react-native';
import MapView from 'react-native-maps';
import RNGooglePlaces from 'react-native-google-places';
import {check, request, PERMISSIONS, RESULTS} from 'react-native-permissions';
import Geolocation from 'react-native-geolocation-service';
import Geocoder from 'react-native-geocoding';
import Config from 'react-native-config';
import {AppContext} from '../../GlobalContext';
import getSubTotal from '../helpers/getSubTotal';
import {regionFrom} from '../helpers/location';
const GOOGLE_API_KEY = Config.GOOGLE_API_KEY;
Geocoder.init(GOOGLE_API_KEY);
Next, create the component class and initialize the state:
class OrderSummary extends Component {
static navigationOptions = {
title: 'Order Summary',
};
static contextType = AppContext;
state = {
customer_address: '',
customer_location: null,
restaurant_address: '',
restaurant_location: null,
};
// next: add componentDidMount
}
Once the component is mounted, we check for the location permissions using the
React Native Permissions library. If the permission is denied
, it means that it has not been requested (or is denied but still requestable) so we request for it from the user. If the user agrees, the permission becomes granted
. From there, we get the user’s current location using the React Native Geolocation Services library. To get the name of the place, we use the React Native Geocoding library to transform the coordinates that we got back. The regionFrom()
function gives us an object which we can supply to React Native Maps to render the location in the map. This function is included in the starter
branch:
let location_permission = await check(
PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
);
if (location_permission === 'denied') {
location_permission = await request(
PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
);
}
if (location_permission == 'granted') {
Geolocation.getCurrentPosition(
async position => {
const geocoded_location = await Geocoder.from(
position.coords.latitude,
position.coords.longitude,
);
let customer_location = regionFrom(
position.coords.latitude,
position.coords.longitude,
position.coords.accuracy,
);
this.setState({
customer_address: geocoded_location.results[0].formatted_address,
customer_location,
});
},
error => {
console.log(error.code, error.message);
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 10000,
},
);
}
// next: add render()
Here’s the render()
method:
render() {
const subtotal = getSubTotal(this.context.cart_items);
const {customer_address, customer_location} = this.state;
return (
<View style={styles.wrapper}>
<View style={styles.addressSummaryContainer}>
{customer_location && (
<View style={styles.mapContainer}>
<MapView style={styles.map} initialRegion={customer_location} />
</View>
)}
<View style={styles.addressContainer}>
{customer_address != '' &&
this.renderAddressParts(customer_address)}
<TouchableOpacity
onPress={() => {
this.openPlacesSearchModal();
}}>
<View style={styles.linkButtonContainer}>
<Text style={styles.linkButton}>Change location</Text>
</View>
</TouchableOpacity>
</View>
</View>
<View style={styles.cartItemsContainer}>
<FlatList
data={this.context.cart_items}
renderItem={this.renderCartItem}
keyExtractor={item => item.id.toString()}
/>
</View>
<View style={styles.lowerContainer}>
<View style={styles.spacerBox} />
{subtotal > 0 && (
<View style={styles.paymentSummaryContainer}>
<View style={styles.endLabelContainer}>
<Text style={styles.priceLabel}>Subtotal</Text>
<Text style={styles.priceLabel}>Booking fee</Text>
<Text style={styles.priceLabel}>Total</Text>
</View>
<View>
<Text style={styles.price}>${subtotal}</Text>
<Text style={styles.price}>$5</Text>
<Text style={styles.price}>${subtotal + 5}</Text>
</View>
</View>
)}
</View>
{subtotal == 0 && (
<View style={styles.messageBox}>
<Text style={styles.messageBoxText}>Your cart is empty</Text>
</View>
)}
{subtotal > 0 && (
<View style={styles.buttonContainer}>
<Button
onPress={() => this.placeOrder()}
title="Place Order"
color="#c53c3c"
/>
</View>
)}
</View>
);
}
Here’s the renderAddressParts()
method. All it does is render the individual parts of the address (street address, town name, etc.):
renderAddressParts = customer_address => {
return customer_address.split(',').map((addr_part, index) => {
return (
<Text key={index} style={styles.addressText}>
{addr_part}
</Text>
);
});
};
When the user clicks on the Change location button link, we use the React Native Google Places library to open a model which allows the user to pick a place. Note that this already gives us the actual name of the place so we don’t need to use the Geocoding library again:
openPlacesSearchModal = async () => {
try {
const place = await RNGooglePlaces.openAutocompleteModal(); // open modal for picking a place
const customer_location = regionFrom(
place.location.latitude,
place.location.longitude,
16, // accuracy
);
this.setState({
customer_address: place.address,
customer_location,
});
} catch (err) {
console.log('err: ', err);
}
};
Here’s the renderCartItem()
method:
renderCartItem = ({item}) => {
return (
<View style={styles.cartItemContainer}>
<View>
<Text style={styles.priceLabel}>
{item.qty}x {item.name}
</Text>
</View>
<View>
<Text style={styles.price}>${item.price}</Text>
</View>
</View>
);
};
Here’s the placeOrder()
method. This extracts the customer location (coordinates) and address from the state, as well as the restaurant location and address from the context. We know that the user can only order from one restaurant, so we can simply get the first item and be assured that it’s the same for all the other items in the cart. Once we have all the required data, we simply pass it as a navigation param to the TrackOrder screen:
placeOrder = () => {
const {customer_location, customer_address} = this.state;
const {
address: restaurant_address,
location: restaurant_location,
} = this.context.cart_items[0].restaurant; // get the address and location of the restaurant
this.props.navigation.navigate('TrackOrder', {
customer_location,
restaurant_location,
customer_address,
restaurant_address,
});
};
TrackOrder screen
Next, we now proceed to the TrackOrder screen. This is where the user can keep track of the progress of their order via a map interface. The map displays markers for their location, the restaurant’s location, and the driver’s location. It also displays the path between those locations.
Start by importing the packages we need:
// src/screens/TrackOrder.js
import React, {Component} from 'react';
import {View, Text, Button, Alert, StyleSheet} from 'react-native';
import MapView from 'react-native-maps';
import Geolocation from 'react-native-geolocation-service';
import MapViewDirections from 'react-native-maps-directions';
import Pusher from 'pusher-js/react-native';
import Config from 'react-native-config';
const CHANNELS_APP_KEY = Config.CHANNELS_APP_KEY;
const CHANNELS_APP_CLUSTER = Config.CHANNELS_APP_CLUSTER;
const CHANNELS_AUTH_SERVER = 'YOUR NGROK HTTPS URL/pusher/auth';
const GOOGLE_API_KEY = Config.GOOGLE_API_KEY;
import {regionFrom} from '../helpers/location';
import {AppContext} from '../../GlobalContext';
Next, add the array which contains the status messages for the order. Each of these items will be displayed as the driver updates the order status on their side:
const orderSteps = [
'Finding a driver',
'Driver is on the way to pick up your order',
'Driver has picked up your order and is on the way to deliver it',
'Driver has delivered your order',
];
Next, create the component class and initialize the state:
class TrackOrder extends Component {
static navigationOptions = ({navigation}) => {
return {
title: 'Track Order',
};
};
static contextType = AppContext;
state = {
isSearching: true, // whether the app is still searching for a driver
hasDriver: false, // whether there's already a driver assigned to the order
driverLocation: null, // the coordinates of the driver's location
orderStatusText: orderSteps[0], // display the first message by default
};
// next: add the constructor()
}
In the constructor, get the navigation params that we passed earlier from the OrderSummary screen. After that, initialize the instance variables that we will be using:
constructor(props) {
super(props);
this.customer_location = this.props.navigation.getParam(
'customer_location',
); // customer's location
this.restaurant_location = this.props.navigation.getParam(
'restaurant_location',
);
this.customer_address = this.props.navigation.getParam('customer_address');
this.restaurant_address = this.props.navigation.getParam(
'restaurant_address',
);
this.available_drivers_channel = null; // the pusher channel where all drivers and customers are subscribed to
this.user_ride_channel = null; // the pusher channel exclusive to the customer and driver in a given order
this.pusher = null; // pusher client
}
// next: add componentDidMount()
On componentDidMount()
is where we initialize the Pusher client and subscribe to the channel where we can look for available drivers. Once subscribed, we trigger an event to request for a driver. We’re putting it inside setTimeout()
to ensure that the connection has really been initialized properly. The event contains all the relevant information that we got from the previous screen:
componentDidMount() {
this.setState({
isSearching: true,
});
this.pusher = new Pusher(CHANNELS_APP_KEY, {
authEndpoint: CHANNELS_AUTH_SERVER,
cluster: CHANNELS_APP_CLUSTER,
encrypted: true,
});
this.available_drivers_channel = this.pusher.subscribe(
'private-available-drivers',
);
this.available_drivers_channel.bind('pusher:subscription_succeeded', () => {
// make a request to all drivers
setTimeout(() => {
this.available_drivers_channel.trigger('client-driver-request', {
customer: {username: this.context.user_id},
restaurant_location: this.restaurant_location,
customer_location: this.customer_location,
restaurant_address: this.restaurant_address,
customer_address: this.customer_address,
});
}, 2000);
});
// next: subscribe to user-ride channel
}
Note: This is an overly simplified driver request logic. In a production app, you will need to filter the drivers so that the only one’s who receives the request are the one’s that are nearby the restaurant and the customer. The code above basically sends a request to all of the drivers.
Next, we subscribe to the current user’s own channel. This will be the means of communication between the driver (the one who responded to their request) and the customer. We listen for the client-driver-response
event to be triggered from the driver’s side. When this happens, we send back a yes
or no
response. If the customer hasn’t found a driver yet, then we send a yes
, otherwise no
. Once the driver receives a yes
response, they trigger the client-found-driver
event on their side. This is then received by the customer and uses it to update the state with the driver’s location:
this.user_ride_channel = this.pusher.subscribe(
'private-ride-' + this.context.user_id,
);
this.user_ride_channel.bind('client-driver-response', data => {
// customer responds to driver's response
this.user_ride_channel.trigger('client-driver-response', {
response: this.state.hasDriver ? 'no' : 'yes',
});
});
this.user_ride_channel.bind('client-found-driver', data => {
// found driver, the customer has no say about this.
const driverLocation = regionFrom(
data.location.latitude,
data.location.longitude,
data.location.accuracy,
);
this.setState({
hasDriver: true,
isSearching: false,
driverLocation,
});
Alert.alert(
'Driver found',
"We found you a driver. They're on their way to pick up your order.",
);
});
// next: subscribe to driver location change
As the driver goes to process the order, their location is constantly watched and sent to the customer via the client-driver-location
event. We use this to update the marker on the map which represents the driver’s location:
this.user_ride_channel.bind('client-driver-location', data => {
// driver location received
let driverLocation = regionFrom(
data.latitude,
data.longitude,
data.accuracy,
);
// update the marker representing the driver's current location
this.setState({
driverLocation,
});
});
Next, listen for the client-order-update
event. This uses the step
value to update the order status. When the driver accepts an order, step 1
is sent. When the driver receives the order from the restaurant, they need to click a button to trigger step 2
to be sent, and so on:
this.user_ride_channel.bind('client-order-update', data => {
this.setState({
orderStatusText: orderSteps[data.step],
});
});
Here’s the render()
method:
render() {
const {driverLocation, orderStatusText} = this.state;
return (
<View style={styles.wrapper}>
<View style={styles.infoContainer}>
<Text style={styles.infoText}>{orderStatusText}</Text>
<Button
onPress={() => this.contactDriver()}
title="Contact driver"
color="#c53c3c"
/>
</View>
<View style={styles.mapContainer}>
<MapView
style={styles.map}
zoomControlEnabled={true}
initialRegion={this.customer_location}>
<MapView.Marker
coordinate={{
latitude: this.customer_location.latitude,
longitude: this.customer_location.longitude,
}}
title={'Your location'}
/>
{driverLocation && (
<MapView.Marker
coordinate={driverLocation}
title={'Driver location'}
pinColor={'#6f42c1'}
/>
)}
<MapView.Marker
coordinate={{
latitude: this.restaurant_location[0],
longitude: this.restaurant_location[1],
}}
title={'Restaurant location'}
pinColor={'#4CDB00'}
/>
{driverLocation && (
<MapViewDirections
origin={driverLocation}
destination={{
latitude: this.restaurant_location[0],
longitude: this.restaurant_location[1],
}}
apikey={GOOGLE_API_KEY}
strokeWidth={3}
strokeColor="hotpink"
/>
)}
<MapViewDirections
origin={{
latitude: this.restaurant_location[0],
longitude: this.restaurant_location[1],
}}
destination={{
latitude: this.customer_location.latitude,
longitude: this.customer_location.longitude,
}}
apikey={GOOGLE_API_KEY}
strokeWidth={3}
strokeColor="#1b77fb"
/>
</MapView>
</View>
</View>
);
}
Channels authentication server
Now let’s proceed with the authentication server. Start by updating the server/.env
file with your Channels app instance credentials:
PUSHER_APP_ID="YOUR PUSHER APP ID"
PUSHER_APP_KEY="YOUR PUSHER APP KEY"
PUSHER_APP_SECRET="YOUR PUSHER APP SECRET"
PUSHER_APP_CLUSTER="YOUR PUSHER APP CLUSTER"
Next, import the packages we need:
// server/index.js
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const Pusher = require('pusher');
Initialize the Node.js client for Channels:
var pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: process.env.PUSHER_APP_CLUSTER,
});
Import the foods data. This contains all of the data about a specific food that we’re going to need:
const {foods} = require('./data/foods.js');
Next, initialize the Express server with the request body parsers and CORS plugin. Also, set the static files location to the images
folder. This allows us to serve the images from the /images
path:
const app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
app.use(cors());
app.use('/images', express.static('images'));
Next, add the route for authenticating the users. The Channels client on the app makes a request to this route when it initializes the connection. This allows the user to trigger events directly from the client side. Note that this will authenticate the users immediately. This is only to simplify things. On a production app, you have to include your authentication code to check if the user who made the request is really a user of your app:
app.post('/pusher/auth', function(req, res) {
var socketId = req.body.socket_id;
var channel = req.body.channel_name;
var auth = pusher.authenticate(socketId, channel); // authenticate the request
res.send(auth);
});
Lastly, expose the server:
const PORT = 5000;
app.listen(PORT, err => {
if (err) {
console.error(err);
} else {
console.log(`Running on ports ${PORT}`);
}
});
Running the app
At this point we’re now ready to run the app. Start by running the server and exposing it via ngrok:
node server/index.js
~/Downloads/ngrok http 5000
Then update the .env
file with your HTTPS URL.
Finally, run the app:
react-native run-android
As we haven’t created the driver app yet, you’ll only be able to test out the first three screens. The TrackOrder screen can only be tested once we create the driver app on the second part of this series.
Conclusion
That’s it for the first part of this series. In this part, you learned how to create a very simple food ordering app using React Native. Specifically, you learned how to use various packages for easily implementing such app. We used React Native Maps to indicate the user’s, restaurant’s, and driver’s location on the map. Then we used React Native Maps Directions to indicate the path between those points.
Stay tuned for part two where we will add the code for the driver app and feature for contacting the driver.
You can find the app’s code on this GitHub repo.
23 October 2019
by Wern Ancheta