Build a voice product review system using Twilio voice and Ngrok

Build a voice product review system using Twilio voice and Ngrok

Introduction

In the world of e-commerce, businesses are constantly striving to enhance customer experiences and improve their operations. One exciting trend is the integration of voice-enabled technologies to revolutionize the way customers interact with online stores. By leveraging the power of Twilio Voice and Ngrok, businesses can create a cutting-edge voice product rating system that offers a seamless and convenient way for customers to provide feedback using their voice.

In this tutorial, we will guide you through the process of building a voice product rating system using Twilio Voice and Ngrok. We will explore how to set up webhooks, handle incoming calls from customers, process product ratings and feedback, and provide real-time responses. By following this tutorial, you will gain the knowledge and tools necessary to integrate a voice product rating system into your e-commerce platform.

By the end of this tutorial, you will have:

  • A server capable of receiving incoming voice calls and processing customer ratings and feedback.

  • Seamless integration with the Twilio Voice API to handle voice communication.

  • Real-time feedback to customers regarding their product ratings.

  • A secure tunnel created using Ngrok to expose your local server to the internet.

Let's get started

Prerequisites

To successfully complete this tutorial, you will need the following:

  • A free Twilio account: To continue with this tutorial, you will need a Twilio free account. By registering through this link, you'll receive $10 in Twilio credit when you upgrade to a paid account.

  • Node.js (version 14.16.1 or higher): Make sure you have Node.js installed on your machine.

  • Npm: We will be using npm, the package manager for Node.js, to install and manage dependencies for our server application.

  • React: Familiarity with React

  • GitHub: Basic knowledge of Git and GitHub will be useful for version control.

  • Ngrok: Ngrok will be used to create a secure tunnel to expose your local server to the internet.

Get the starter code

The first step is to choose a spot on your local computer for the project to be installed. To clone the starter code repository, open the terminal and do the following command.

git clone https://github.com/Titre123/Twilio-Ecommerce.git

Install dependencies and set environment variables

We can now install the dependencies now that the starter project is already in a directory on your local computer. To install dependencies, execute the following command.

npm install

After we've installed all of the dependencies that the application will require, let's set the environment variables that our application will require. To copy the .env.example file into .env, use the following command.

cp .env.example .env

Now you have a copy of the .env.example file in the .env file, we can now set the variable defined in the .env file.

In your code editor, open the.env file. This is what your code editor should see.

NODE_ENV = development
PORT =<>
MONGO_URI = <>
JWT_SECRET = <>
PAYPAL_CLIENT_ID= <>

Set the PORT to 8000. The JWT_SECRET will be decided by you and you can get your MONGO_URI from mongo atlas. PAYPAL_CLIENT_ID can be gotten from the paypal developer page.

Run the application

You can now launch the application after installing the dependencies and configuring the environment variables.

Starting the backend server

To start the application's backend, type the following command into the terminal.

npm run server

After running the program, you should see the following log on your terminal.

Server running in development mode on port 8000
MongoDB Connected: ac-zba4w9i-shard-00-01.9yxqrd1.mongodb.net

Starting the frontend application

To launch the frontend. Run the following commands in a second terminal.

cd frontend

The command above will navigate to the frontend directory

npm start

A few minutes after you run the command, your default browser will open up a tab point to the url http://localhost:3000/. The application home page will look like this.

Configuring Twilio credentials and access tokens

Retrieving Twilio credentials from the Twilio console

Next, log into your Twilio console to retrieve your Twilio credentials, which include your Token, SID, and phone number. These credentials will be stored as environment variables.

Adding Twilio credentials to the .env file

To set up the appropriate environment variables, add the Twilio credentials to your .env file. To launch the frontend. Run the following commands in a second terminal. After running the program, you should see the following log on your terminal.

NODE_ENV = development
PORT = <>
MONGO_URI = <>
JWT_SECRET = <>
PAYPAL_CLIENT_ID = <>
TWILIO_TOKEN = <>
TWILIO_SID = <>
TWILIO_PHONE = <>

Implement calling placing from application

Implement backend to call when a client make an order

Next, create a twilio.js file in the utils directory in your backend directory and copy the following code below inside.

import twilio from 'twilio';
const accountSid = process.env.TWILIO_SID;
const authToken = process.env.TWILIO_TOKEN;
const phoneNo =process.env.TWILIO_PHONE;
const client = twilio(accountSid, authToken);

export default async function makeCall(twimlUrl, recipient) {

try {
    const call = await client.calls.create({
        url: twimlUrl, // URL for your TwiML
        to: recipient, // The recipient's phone number
        from: phoneNo // Your Twilio phone number
    });

    console.log('Call SID:', call.sid);
} catch (error) {
    console.error('Error:', error.message);
    }
}

The code above set up twilio in the backend. It imports the Twilio package and set client up with the accountSid and authToken. makeCall function calls the recipient from the Twilio phone number.

Implementing the call placing logic in backend

Next, import the makeCall function and call the function when an order is created.

Navigate to the orderController.js in the controller directory and import the makeCall function below import Order from '../models/orderModel.js'

import makeCall from '../utils/twilio.js'
import User from '../models/userModel.js'

Update the addOrderItems function in orderController.js to call the makeCall function after order has been created.

/ @desc    Create new order
// @route   POST /api/orders
// @access  Private
const addOrderItems = asyncHandler(async (req, res) => {
const {
    orderItems,
    shippingAddress,
    paymentMethod,
    itemsPrice,
    taxPrice,
    shippingPrice,
    totalPrice,
} = req.body

// Check if there are order items

if (orderItems && orderItems.length === 0) {
    res.status(400)
    throw new Error('No order items')
    return
} else {
    const order = new Order({
    orderItems,
    user: req.user._id,
    shippingAddress,
    paymentMethod,
    itemsPrice,
    taxPrice,
    shippingPrice,
    totalPrice,
})
const user = await User.findById(user);
// Save new order to the database
const createdOrder = await order.save()
await makeCall(‘', user.phoneNumber)
res.status(201).json(createdOrder)
}
})

Set up TwiMl Webhook

Select a location in your local computer that you want to set up the webhook project. Open the terminal and run the following command to clone the webhook starter repository to your local computer.

git clone https://github.com/Titre123/webhook.git

Install dependencies for webhook

Now that the starter project is already in a directory in your local computer, we can now install the dependencies. Run the following command to install dependencies.

npm install

Run the webhook

After you have installed the dependencies, you can now run the webhook. Run the following command in the terminal to start the webhook.

npm run dev

Create a .env file and define a TWILIO_ACCOUNT_SID environment variable.

You should see the following log on your terminal a few seconds after running the command.

Server running on port 4000

Webhook to handle outgoing calls

Set up utility functions

Next, navigate to the utils.js file, just below the import statement, add the following functions.

const jwt = require('jsonwebtoken');
const axios = require('axios');
const generateTokenWithPhoneNumber = (phoneNumber) => {
    return jwt.sign({ phoneNumber }, process.env.JWT_SECRET, { expiresIn: '30d' })
}
const generateToken = (id) => {
    return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '30d' })
}
async function getUserByPhoneNUmber(phoneNumber) {
    let response;
    const token = generateTokenWithPhoneNumber(phoneNumber);
    try{
        response = await axios.get(`http://localhost:8000/api/users/phone/${phoneNumber}`, {
        headers: {
        Authorization: Bearer ${token}
        }
    });
    console.log(response.data);
    return response.data._id;
    } catch (error) {
        console.error('API request failed:', error.message);
        throw error;
    }
}
async function createToken(phoneNumber) {
    const id = await getUserByPhoneNUmber(phoneNumber);
    const token = generateToken(id);
    return token;
}
async function callOrderProduct(orderId, phoneNumber) {
    let response;
    const token = await createToken(phoneNumber);
    try {
        response = await axios.get(`http://localhost:8000/api/orders/${orderId}`, {
        headers: {
            Authorization: Bearer ${token}
        }
    })
    return response.data.orderItems;
    } catch (error) {
        console.error('API request failed:', error.message);
        throw error;
    }
}
async function reviewProduct(productId, phoneNumber, rating, feedback) {
    let response;
    const token = await createToken(phoneNumber);
    const requestBody = {
    rating,
    feedback
    // Add more properties as needed
    };
    try {
        response = await axios.post(`http://localhost:8000/api/products/${productId}/reviews`, requestBody, {
        headers: {
            Authorization: Bearer ${token}
        },
    })
    return response.data;
    } catch (error) {
        console.error('API request failed:', error.message);
        throw error;
    }
}

generateTokenWithPhoneNumber generates a token using the phone number. generateToken generates a token using user id. getUserByPhoneNUmber gets a user from the database based on the phone number calling. createToken generates the token that we will use to query the backend. callOrderProduct gets an order based on the orderId provided. reviewProduct reviews a product with the productId.

Export callOrderProduct and reviewProduct in the utils.js. To export, add the following export statement below the reviewProduct function.

module.exports = {
    callOrderProduct, reviewProduct
}

Create webhook routes to make calls.

Next, navigate to the app.js and add the following post routes below app.use(express.urlencoded({ extended: true }));

app.post('/twiml', (req, res) => {
const twiml = new twilio.twiml.VoiceResponse();
const phoneNumber = req.body.phoneNumber;
const orderId = req.query.orderId;
twiml.say(
{ voice: 'alice' },
'Welcome to the Product Review System. Would you be willing to review the products in your recent order? Press 1 to accept or press 2 to reject.'
);
twiml.gather({
input: 'dtmf',
timeout: 10,
numDigits: 1,
action: /handle-response?phoneNumber=${phoneNumber}?orderId=${orderId},
method: 'POST',
});
res.set('Content-Type', 'text/xml');
res.send(twiml.toString());
});
app.post('/handle-response', async (req, res) => {
const { Digits } = req.body;
const phoneNumber = req.query.phoneNumber;
const orderId = req.query.orderId;
if (Digits === '1') {
const products = await callOrderProduct(orderId, phoneNumber);
for (const currentProduct of products) {
const twiml = new twilio.twiml.VoiceResponse();
twiml.say(
    { voice: 'alice' },
    Please rate ${currentProduct.name} on a scale of 1 to 5. Press any key to begin.
);
twiml.gather({
    input: 'dtmf speech',
    timeout: 10,
    numDigits: 1,
    action: /handle-rating?phoneNumber=${phoneNumber}&productId=${currentProduct.product},
    method: 'POST',
    numDigitsTimeout: 1,
});

res.set('Content-Type', 'text/xml');

res.send(twiml.toString());

}

} else if (Digits === '2') {
    // End the call if the user rejects reviewing the products
    const twiml = new twilio.twiml.VoiceResponse();
    twiml.say({ voice: 'alice' }, 'Thank you. Goodbye!');
    twiml.hangup();
    res.set('Content-Type', 'text/xml');
    res.send(twiml.toString());
    } else {

    // Invalid input, redirect to welcome message

    const twiml = new twilio.twiml.VoiceResponse();
    twiml.redirect('/twiml');
    res.set('Content-Type', 'text/xml');
    res.send(twiml.toString());
    }

});

The /twiml endpoint prompts the caller to review products or reject the request. The caller's response is collected through DTMF tones and sent to the /handle-response endpoint. If the caller accepts, the server fetches the products and requests ratings for each one. If the caller declines, the call is terminated with a goodbye message. Incorrect input returns the caller to the /twiml endpoint.

Processing customer ratings and feedback

Now, we have routes that get user feedback and rating

app.post('/handle-rating', async (req, res) => {
    const { Digits, SpeechResult } = req.body;
    const { phoneNumber, productId } = req.query;
    let rating = Digits;
    let feedback ="";
if (SpeechResult) {
    const twilioClient = twilio();

    const recording = await twilioClient.api.v2010.accounts(process.env.TWILIO_ACCOUNT_SID).recordings(req.body.SpeechResult).fetch();

    if (recording && recording.transcription && recording.transcription.transcriptionText) {
        feedback = recording.transcription.transcriptionText;
    }
}

await reviewProduct(`/products/${productId}/reviews`, phoneNumber, rating, feedback);
    // Respond with an empty TwiML to end the call
    const twiml = new twilio.twiml.VoiceResponse();
    res.set('Content-Type', 'text/xml');
    res.send(twiml.toString());
});

The user's rating and comments for a particular product are sent to the /handle-rating endpoint. The phoneNumber and productId are taken out of the query parameters, together with the Digits and SpeechResult from the request body. The reviewProduct function is then called asynchronously to store the review data using the rating and feedback variables. Finally, the call is terminated by sending an empty TwiML VoiceResponse.

Creating a Secure Tunnel with Ngrok

Installing and configuring Ngrok

To use the webhook in our application, we must first install ngrok. Ngrok will provide us with a domain for hosting the webhook.

This is a screenshot of the Ngrok dashboard. Download and unpack Ngrok, then set up environment variables to allow you to execute it from the terminal.

You can now begin utilizing ngrok when you have finished setting up Ngrok and configuring it with your ngrok authentication key in the console.

Testing the secure tunnel functionality

Start ngrok and construct a tunnel to your localhost port 4000 by opening a new terminal and entering the following command.

ngrok http 4000

Ngrok has started HTTP tunnel forwarding to your local port 4000 where my webhook is being hosted.

Add Ngrok url to backend

Next, add the url from the ngrok terminal to the backend in order to make calls from your applicaton.

Navigate back to the orderController.js and add the ngrok url to the makeCall function call in the addOrderItems function.

await makeCall('', user.phoneNumber)

Testing of Application

Now is a great time to test out your application to see it in action.

Go to the homepage and order some products.

Now let’s go and re-order the product.

Now we can see that the review is already added.

What is next for your voice product review system

Now that you have added this cool whatsapp functionality to your ecommerce store, there are many other things that you can still do like deleting orders when a user does not confirm an order or creating an order for the user based on the prompt provided.

Additional resources

Twilio Voice - This page contains documentation on Twilio Voice.

TwiML - This page contains documentation on TwiML.

Twilio SDK for Node - The github repository contains Twilio Node SDK.

Taiwo Ola-Balogun is a Software Developer at ALX AFRICA. He is very passionate about javascript and bash and also enjoys building amazing software. He can be reached at taiwotriumphant [at] gmail.com.