Build a food ordering app using Vue and Africa’s Talking
Some knowledge of HTML, CSS and JavaScript is required. Knowledge of Vue.js would be an advantage, but not essential.
Thoughts on building for the next billion users? Development systems are springing up by the day with progressive web apps taking center stage and optimization being the focal point of quality testing teams. We live in a world where using digital services almost always requires the Internet. Is this scalable to even remote parts of the world where internet connectivity is next to nothing?
USSD (Unstructured Supplementary Service Data) communication technology used mostly by telecommunication companies offers much more promise than just interaction with your telco providers. In this post, we will be building a basic food-ordering App using Vue and implementing USSD interaction via Africa’s Talking(AT).
The app consists of two parts: the USSD app for the user and the web dashboard for the App owner. Africa’s Talkin (AT) will be used to build the USSD app and Vue for the app dashboard. Pusher will be used to implement the real-time update of our dashboard. You can use services like Twilio or txtNation as alternative to AT if you can’t access the AT services.
Here is a quick look at what we will be building:
Prerequisites
Building this app is beginner friendly. However, knowledge of HTML, CSS, and JavaScript is required. Using Pusher and Africa’s Talking is pretty much seamless. Vue.js will be used to develop the user interface and its knowledge is not required but is of added advantage.
Installation
This is a Node.js app so it requires you have node and its corresponding package manager, NPM. Install them from here if you haven’t, otherwise verify installation from your command line with:
node -v && npm -v
Create a folder anywhere on your machine, this will be the root directory of your app. Start a new project by running this on your command line:
npm init
Run through the setup instructions filling out key app details like name, author, and license.
Note that the entry point specified during npm init (default:index.js), this is the file in which server configuration will take place.
Once this is completed, you have a new node project with a package.json file but otherwise pretty much empty. Let’s install the required tools and dependencies via npm. Dependencies required are:
- Express - A node framework for developing servers.
- Cors - Enables cross-origin requests.
- Pusher - This is the node package for pusher.
- Body-parser - Parses the req.body object.
- Africastalking - The node package for Africa’s talking.
Install these locally with:
npm install --save express cors pusher body-parser africastalking
During development, Vue will be imported via a CDN so don’t worry if you don’t see it being installed yet. Let’s get to configuring our server since this will serve our files.
We will proceed to create an account on Pusher and Africa’s Talking.
Create a new Pusher app with whichever name you choose. Note the AppID, key, and cluster issued. Do the same with Africa’s Talking, create a new USSD application and obtain an API key and the username of the app. For this app, we will be utilizing the AT sandbox to test our app, so there is no need to apply for a service code.
To keep credentials safe, we will create a new module to house these credentials.
In the root directory create a new file called cred.js and setup the module like this:
module.exports = {
AT:{
apiKey: 'xxxxxxxxxxxxxxxxxxxxxxx',
username: 'xxxxxxx',
format: 'json'
},
pusher:{
appId: 'xxxxxx',
key: 'xxxxxxxxxxxxxxxx',
secret: "xxxxxxxxxxxxxx",
cluster: "xx",
encrypted: true
}
}
The credentials are stored in an object and will be used later in our app.
Build the app server
In the root directory of the project, create a new file called server.js. In this file we will configure our server. First, import all required dependencies for the app and assign them to variables with:
var Pusher = require('pusher')
var credentials = require('./cred')
var africastalking = require('africastalking')(credentials.AT)
var cors = require('cors')
var bodyParser = require('body-parser')
var express = require('express')
var path = require('path')
Since we will be using an express server, configure express by first creating a new instance assigned to a variable:
var app = express()
var port = 3000
We simply assigned the port number of the app to 3000. This is the port on the local server which the app will be available.
Let’s use other imported modules like cors and bodyParser:
app.use(cors())
app.use(bodyParser.urlencoded({extended:false}))
app.use(bodyParser.json())
Next, create an endpoint for your homepage using express with:
app.get('/', function(req, res){
res.sendFile(path.join(__dirname + "/index.html"))
})
The index.html file hasn’t been created yet, but it would be served nonetheless when created.
We would require other static files such as CSS and JS in our HTML script, use the express.static()
method to specify the directory of the files. These static files will be in our root directory as well:
app.use(express.static(__dirname + '/'))
It’s not safe to expose static files like this, but we’re just doing this for convenience. If you do this for real, I can make a request to
GET /cred.js
and get your AT and Pusher credentials.
Create a new Pusher instance and assign it to a variable, just so it is available globally:
var pusher = new Pusher(credentials.pusher)
Remember the module we created earlier and imported - cred.js. Pusher requires your app credentials when initializing a pusher instance.
Working with USSD is pretty simple, we create a POST endpoint and configure responses per users request using IF statements.
Africa’s talking receives these requests on a USSD code we will set on their service and responds with the messages we will configure. We simply control what is sent to the user and determine what the user receives when they make a new request. AT simply provides the offline channel for the user via USSD.
Next, we will configure AT. First, we create some important global variables with:
var webURL = 'http://foodigo.com/menu'
var welcomeMsg = `CON Hello and welcome to Foodigo.
Have your food delivered to you fast and hot!
Please find our menu ${webURL}
Enter your name to continue`
var orderDetails = {
name: "",
description: "",
address: "",
telephone: "",
open: true
}
var lastData = "";
The welcomeMsg
variable is assigned a template literal with the welcome message on our USSD app. The orderDetails
object is the payload to be sent over to the dashboard via Pusher.
This is the more interesting part. Create the REST API with a POST request on a /order
route:
app.post('/order', (req,res) => {
console.log(req.body)
//configure /order api
})
When a user makes a request via a certain USSD service code, the body of the request contains useful information about the request which we will process. This information is to be passed to variables as required. The properties in the req.body
object are:
- sessionID - For every time a user dials a USSD code, a session is opened. The sessionId is a unique ID assigned to each session opened by a user.
- serviceCode - This is the service code dialed by a user, also note that multiple service codes can be assigned to an app.
- phoneNumber - The telephone number of the user is also sent through the request, You can create a database from this and play around with the data.
- text - This is the plain text inputted by the user in the text field. Initially, when a user initiates a session, text is empty.
In the /order
API, assign the various req.body
parameters to variables. For beginners understanding, do this:
...
var sessionId = req.body.sessionId
var serviceCode = req.body.serviceCode
var phoneNumber = req.body.phoneNumber
var text = req.body.text
var textValue = text.split('*').length
...
What about the response from our server to the client? Let’s call that message
, create the variable and assign it an empty value. To understand the current state of response from the server, we use some simple logic to split the response carrying user information. The request (text
) comes in plain text with user inputs separated by asterisks (*). Subsequent inputs are concatenated to the existing request and each is separated by an asterisk. By splitting text
with the asterisks, the length of the resulting array would better inform us on how many requests has been made by the user.
var message = ""
Next, we define the logic with which AT will process user requests and responses using if
statements. In the /order
endpoint include this logic:
...
if(text === ''){
message = welcomeMsg
}else if(textValue === 1){
message = "CON What do you want to eat?"
orderDetails.name = text;
}else if(textValue === 2){
message = "CON Where do we deliver it?"
orderDetails.description = text.split('*')[1];
}else if(textValue === 3){
message = "CON What's your telephone number?"
orderDetails.address = text.split('*')[2];
}else if(textValue === 4){
message = `CON Would you like to place this order?
1. Yes
2. No`
lastData = text.split('*')[3];
}else{
message = `END Thanks for your order
Enjoy your meal in advance`
orderDetails.telephone = lastData
}
...
If statements simply serve our purpose here. In the block above it simply means once a user makes the initial request by dialling the service code the response should be the welcomeMsg
variable defined previously. For the sake of simplicity of this demo, we wouldn’t be considering edge cases and responses to match the request. Once a response is received as text
, it is manipulated and assigned to a parameter in the orderDetails
object.
Messages begin with CON
, this shows the beginning of the response and the session as well and to terminate the session the response starts with END
.
Still in the /order
endpoint, specify the response with:
...
res.contentType('text/plain');
res.send(message, 200);
...
Now that we have a payload in orderDetails
, Pusher is to handle the delivery of this payload to the client dashboard. In the /order
endpoint, open a new Pusher channel and event with:
...
if(orderDetails.name != "" && orderDetails.address != ''&& orderDetails.description != '' && orderDetails.telephone != ''){
pusher.trigger('orders', 'customerOrder', orderDetails)
}
...
We created a simple test to ensure that all the data is collected in orderDetails
before it is sent through Pusher. Use the pusher trigger()
method to open a new pusher channel, in this case, orders
. A new event customerOrder
is created and orderDetails
is passed as the payload.
Lastly, reset orderDetails
to its default state of empty values:
if(orderDetails.telephone != ''){
//reset data
orderDetails.name= ""
orderDetails.description= ""
orderDetails.address= ""
orderDetails.telephone= ""
}
Next, set up the listening port of the app with:
app.listen(port, (err,res) => {
if(err) throw err;
console.log('listening on port ' + port)
})
Run the server with:
node server.js
You will see listening on port 3000
in the command line, but on opening localhost:3000
in your browser an error is thrown. This is fine and it is because we haven’t created the index.html
file to be served by the server. So far we have created the app’s server but, before we test, let’s build out the app dashboard.
Build the dashboard
While we have our server ready to handle client orders, we need a dashboard to display these orders and we will accomplish this using Vue.js.
Vue is a lightweight progressive JavaScript framework for building interactive user interfaces. In the root directory of your project, create the HTML, CSS and JavaScript Files required.
In index.html, create a HTML5 script with:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="index.css">
<title>Real-time food ordering app dashboard</title>
</head>
<body>
<div class="container-fluid" id="root">
<!-- Body of document -->
</div>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.js"></script>
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
<script src="index.js"></script>
</body>
</html>
Note the imported scripts via a CDN - Vue, Pusher. We also imported our external JavaScript file. Bootstrap is also used for styling and is imported from a CDN as seen in the head tag of the script.
For the simplicity of this demo, our orders will be in the form of cards (a presentation UI widget) and have a close button to close any open orders.
Let’s configure Vue in the index.js file created earlier.
new Vue({
el: "#root",
data:{
title: "Foodigo Restaurant Dashboard",
orders:[
{name:"Chris Nwamba", description:"Rice and Ofe-Akwu", address:"Lekki", telephone:"08082092001", open:true},
{name:"William Imoh", description:"Rice and Chicken", address:"Amuwo", telephone:"08082818700", open:true},
{name:"Mary-Anne Unoka", description:"Yam and Egg Sauce", address:"Satellite Town", telephone:"08083872501", open:true},
{name:"Ralph Ugwu", description:"Rice and Salad", address:"Nsukka", telephone:"08082983021", open:true},
{name:"BLAQLSG Imoh", description:"Cake and Sprite", address:"Ije-Ododo", telephone:"08082869830", open:true}
]
},
created(){
var pusher = new Pusher('PusherKey',{
cluster:'PusherCluster',
encrypted:true
})
var channel = pusher.subscribe('orders')
channel.bind('customerOrder', (data) => {
console.log(data)
this.orders.push(data)
})
}
})
In the script above we created a new Vue instance and mounted it on the DOM element with an ID of root
. Basically, state in Vue is managed by the data
property. We will manage our app state here as well as other data which we would like to pass to the DOM using Vue. For this app, we created placeholder data in the orders
array consisting of the name
, description
, address
, telephone
and the order status of either true
or false
.
We used a Vue lifecycle method called the created method to setup pusher. The created()
method is called once the DOM is fully loaded. In it, we configure a pusher instance with the pusher key obtained during account creation.
Next, using the subscribe()
method, we subscribed to the channel we created on our server - orders
. Afterwards, the event created in the server is bound to the channel and it takes a callback function. This function receives a parameter which is the payload sent though pusher, in this case, orderDetails
.
Lastly, we push this data into the orders array. How about if we want to close an order? We will create a method to handle that next.
Closing an order requires the creation of a method to handle the action. So once the method is triggered, the state of the open
property of the order is changed to false
.
In the Vue instance, after the created()
method, create a methods
object and update it like this:
...
methods:{
// close completed order
close(orderToClose){
if ( confirm('Are you sure you want to close the order?') === true){
this.orders = this.orders.map(order => {
if(order.name !== orderToClose.name && order.description !== orderToClose.description){
return order;
}
const change = Object.assign(order, {open: !order.open})
return change;
})
}
}
}
...
A confirm
action is required to ascertain that the order is to be closed and we simply use a map()
method to run through the available orders until we find an order that matches the parameter of the particular order to be closed, at which point the state of its open
property is inverted.
Now we have our data and methods all set up, let’s pass them to the DOM.
In index.html, include this block in the parent div
with an ID of root
:
...
<header>
<h1 class="text-center">{{title}}</h1>
<h4 class="text-center">Realtime food ordering app via USSD</h4>
</header>
<main>
<div class="row">
<div v-for="order in orders" v-if="order.open" class="col-md-4 order-card">
<h3 title="Customer Name">{{order.name}}</h3>
<span class="closeicon" @click="close(order)" title="Close Order">X</span>
<p title="Order Description">{{order.description}}</p>
<p title="Customer Address">{{order.address}}</p>
<p title="Customer Telephone">{{order.telephone}}</p>
</div>
</div>
</main>
...
The title of our app is set dynamically using Vue. In the div with the class of order-card
, we simply used the v-for
directive to iterate over the orders
array. This is done conditionally using the conditional v-if
directive. This means only if order.open
resolves to true will the order be iterable. Each order property is passed to DOM elements using mustache-like syntax.
Style the app in index.css with:
body{
margin: 0px;
padding: 0px;
}
.container-fluid{
margin: 0px;
padding: 0px;
width: 100%;
}
header{
height: 160px;
margin: 0px;
background-color: rgb(240, 75, 75);
padding: 40px;
}
header h1{
margin: 0px;
}
header h4{
color: rgb(92, 91, 91);
}
.completeOrder{
display: none;
}
.closeicon{
font-weight: bold;
position: absolute;
top: 15px;
right: 20px;
cursor: pointer;
}
.closeicon:hover{
opacity: 0.5;
}
.order-card{
background-color: antiquewhite;
margin: 50px;
border-radius: 10px;
width: 20%;
margin-right:0px;
margin-top: 30px;
margin-bottom: 0px;
}
Everything is all set, start the app server with:
node server.js
All we see is placeholder data. We will use the Africa’s talking sandbox app to test our app. Before that, we need to expose our local server to the internet so we can access our endpoint on the Internet. Ngrok will be used for this. Navigate to the directory with ngrok in the command line and expose your local server with:
./ngrok http 3000
Once the session status on the ngrok dashboard in the command line interface goes to online
, a forwarding address is issued. This is the temporary web address for our app e.g. https://1654a6cb.ngrok.io.
Since this address is available on the internet, our POST endpoint is https://1654a6cb.ngrok.io/order. This is the endpoint required by Africa’s Talking.
Log-in to AT and go to the sandbox app here. In the sandbox, create a service code and pass in the callback URL of your API endpoint. Our USSD code is now available for use. In the sandbox app, navigate to the ‘launch simulator’ tab and input a valid telephone number to use the USSD feature. All these should be done while your local server is running and ngrok is online.
Once you dial the USSD code in the simulator, fill out the text fields with the chosen data and, once the responses are complete, the dashboard automatically updates with the new order.
Conclusion
It’s been awesome building this out. We have seen how to integrate USSD interaction in a node app, and so far I can’t help but imagine the immense potential this poses. Vue was used to build out the front-end and pass data received to the DOM, with Pusher bringing in realtime activity so it is unnecessary to refresh the page to know if an order has arrived. Meanwhile anyone with the USSD code can place an order without having internet connectivity. Feel free to play around with the app, suggest improvements and submit improvements to the source code here.
8 March 2018
by Christian Nwamba