🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Create a food ordering app in React Native - Part 1: Making an order

  • Wern Ancheta

October 23rd, 2019
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:

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.

Clone the project repository
  • Android
  • Beams
  • JavaScript
  • Maps
  • Node.js
  • React Native
  • Chat
  • Webhooks
  • Chatkit
  • Channels
  • Beams

Products

  • Channels
  • Chatkit
  • Beams

© 2020 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.