DEV Community

Cover image for Guide to sending SMS with the Twilio API
Joseph Udonsak for Sanity.io

Posted on • Originally published at sanity.io

Guide to sending SMS with the Twilio API

Introduction

In a fast-paced environment, it is becoming increasingly important that software systems keep customers abreast with the latest information - whether or not they are actively interacting with the system. This is especially true in situations where the customer needs to be notified of events that weren't a direct result of their activity on the system (such as an order being made for a product or payment being received for a delivered service).

These days, notifications can either be sent via email, SMS, or even via popular messaging platforms like WhatsApp or Telegram. However, SMS notifications top the bunch because, unlike the other notification mediums, the customer is not required to have an internet connection in order to receive SMS messages. Additionally, SMS messages will be more accessible since there is the chance that more than a few customers may not even have a WhatsApp or Telegram account to start with.

Twilio is a platform that allows you to connect with customers everywhere they want to interact with you in real-time —from SMS messages to emails, phone calls to video, intelligent chatbots, and back. With helper libraries for popular programming languages, Twilio makes managing/dispatching programmable SMS a breeze.

In this tutorial, we'll build an e-commerce web application using Next.js and implement an SMS notification feature. We'll also learn about React Context which will be used to implement the cart functionality. The content for the e-commerce site will be hosted (and served) using Sanity.io. A customer can make a selection of items and purchase them. Once a customer has purchased an item, a text message will be sent to the phone number provided by the customer, confirming the order details and the expected delivery date in the message body. The application we will build can be previewed here. The code for the complete project is available on Github should you get stuck at any point.

Prerequisites

To keep up with the concepts that will be introduced in this article, you will need a fair grasp of JavaScript ES6. While we won't delve too deep into React and Next.js, it will be helpful to have some understanding of both.

You will also need to have the following installed on your workstation:

  1. Node Package Manager (NPM)
  2. Sanity CLI
  3. A Github account
  4. A Twilio account
  5. A code editor of your choice

Getting started

Setting up Sanity

For this article, we'll be using the Next.js E-commerce official starter to build our web application. To get started, open the starter page in your browser. A screenshot of the starter page is shown below

Next.js E-commerce official starter page

Follow the instructions on the starter page to set up the project. In addition to receiving an email with the details of the Vercel deployment and newly created Sanity project, a Github repository will be created for you to clone and start working with. Clone the repository using the following command:

git clone https://github.com/<YOUR_GITHUB_ID>/next-js-e-commerce.git 
sanity-twilio

cd sanity-twilio
Enter fullscreen mode Exit fullscreen mode

This command pulls the repository into a folder named sanity-twilio. Before we start adding code to the project, let's set our environment variables. To do this, make a copy of the .env.test file named .env.

cp .env.test .env
Enter fullscreen mode Exit fullscreen mode

In the .env file, replace the SANITY_STUDIO_API_PROJECT_ID and NEXT_PUBLIC_SANITY_PROJECT_ID with your sanity project id.

We also need to provide the project details for our sanity studio project. In the studio directory, edit the api node in sanity.json as follows:

"api": {
    "projectId": "INSERT_YOUR_PROJECT_ID",
    "dataset": "production"
  },
Enter fullscreen mode Exit fullscreen mode

Next, install the project dependencies using npm

npm install
Enter fullscreen mode Exit fullscreen mode

Start the application using NPM.

npm run dev
Enter fullscreen mode Exit fullscreen mode

By default, the application will run on port 3000. Navigate to http://localhost:3000/ to see the application.

Index page of Next.js E-commerce web application

Next, we need to set up a messaging service on our Twilio account. To do this, head to your Twilio console and click the Create Messaging Service button.

Twilio console with 'Messaging Services' selected

We'll name this service Sanity E-commerce and our use case is to notify the users. Complete the form as shown below and click Create.

Modal form to create messaging service

Next, we need to add a sender to our service. In our case, we need a phone number. Click Add Senders and select Phone Number then click Continue.

Modal displayed when 'Add sender' is clicked

Finally, you will need to add at least one phone number to this service. If you don't have one (or need a new one), you can buy more numbers. Select a phone number and click Add Phone Numbers to complete the set up.

Form to add sender phone number

Click on the Properties menu option to view the Messaging Service SID. Make a copy of this as we will use it to integrate our e-commerce web application with Twilio.

Properties of the newly created messaging service

In addition to the Messaging Service ID, you will also need your Twilio phone number, ACCOUNT SID and AUTH TOKEN to integrate with Twilio. You can find these on your dashboard.

Twilio dashboard showing 'ACCOUNT SID' and 'AUTH TOKEN'

As a security precaution, your AUTH TOKEN will not be shown on screen, click on the copy icon to copy it.

Making the 'Cart' work

At the moment, the cart feature of our application doesn't work. We can't add or remove items to our cart. We can't even see it. Let's fix that before we implement the checkout feature.

To manage the cart in this application, we'll take advantage of the React Context API. To get started, create a file named context.js in the utils directory. This context will control the visibility of the cart and the items in the cart. It will also provide helper functions that will allow us to add or remove items from the cart, or update the quantity of items in the cart. Finally, it will also provide a hook that allows us to access exactly what we need from the context in each component.

touch utils/context.js
Enter fullscreen mode Exit fullscreen mode

Add the following to context.js

// utils/context.js
import { createContext, useContext, useState } from "react";

const CartContext = createContext();

const CartContextProvider = ({ children }) => {
  const [cart, setCart] = useState([]);
  const [cartOpen, setCartVisibility] = useState(false);

  const removeAllItemsFromCart = () => {
    setCart([]);
  };

  const showCart = () => {
    setCartVisibility(true);
  };

  const hideCart = () => {
    setCartVisibility(false);
  };

  const toggleCartVisibility = () => {
    setCartVisibility(oldVisibility => !oldVisibility);
  };

  const findOrderInCart = productId => cart.find(({ id }) => id === productId);

  const updateOrderQuantity = (productId, newQuantity) => {
    const predicate = ({ id }) => id === productId;

    setCart(oldCart => {
      const order = oldCart.find(predicate);
      order.quantity = newQuantity;
      const orderIndex = oldCart.findIndex(predicate);
      const newCart = [...oldCart];
      newCart[orderIndex] = order;
      return newCart;
    });
  };

  const addOrderToCart = order => {
    const orderInCart = findOrderInCart(order.id);
    if (!orderInCart) {
      setCart(oldCart => [...oldCart, order]);
    } else {
      const newOrderQuantity = orderInCart.quantity + order.quantity;
      updateOrderQuantity(order.id, newOrderQuantity);
    }
    showCart();
  };

  const removeOrderFromCart = productId => {
    setCart(oldCart => oldCart.filter(({ id }) => id !== productId));
    showCart();
  };

  return (
    <CartContext.Provider
      value={{
        cart,
        addOrderToCart,
        findOrderInCart,
        removeOrderFromCart,
        updateOrderQuantity,
        cartOpen,
        showCart,
        hideCart,
        toggleCartVisibility,
        removeAllItemsFromCart
      }}>
      {children}
    </CartContext.Provider>
  );
};

export const useCartContext = () => {
  const context = useContext(CartContext);
  if (context === undefined) {
    throw new Error("useCartContext must be used within a CartContextProvider");
  }
  return context;
};

export default CartContextProvider;
Enter fullscreen mode Exit fullscreen mode

Next, we need to wrap the root component of our application with the CartContextProvider. To do this, open pages/_app.js and update it to match the following:

// pages/_app.js
import "../styles/index.css";
import Layout from "../components/Layout";
import CartContextProvider from "../utils/context";

function MyApp({ Component, pageProps }) {
  return (
    <CartContextProvider>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </CartContextProvider>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Next, we need a component that will render a single item in our cart. An item in our cart will be represented as an object comprising of the following:

  1. The product id.
  2. The title (name) of the product.
  3. The slug for the product. This will be used to create a link to the product page.
  4. The price for a single unit of the product.
  5. The number of units the customer intends to order.
  6. The product image.

In the components directory, create a new file called CartItem.js.

touch components/CartItem.js
Enter fullscreen mode Exit fullscreen mode

In CartItem.js add the following:

// components/CartItem.js
import { urlFor } from "../utils/sanity";
import Link from "next/link";
import { useCartContext } from "../utils/context";

const CartItem = ({
                    id,
                    title,
                    slug,
                    price,
                    quantity,
                    mainImage
                  }) => {

  const { removeOrderFromCart, updateOrderQuantity } = useCartContext();

  const handleRemoveButtonClick = () => {
    removeOrderFromCart(id);
  };

  const reduceOrderQuantity = () => {
    if (quantity > 1) {
      updateOrderQuantity(id, quantity - 1);
    } else {
      removeOrderFromCart(id);
    }
  };

  const increaseOrderQuantity = () => {
    updateOrderQuantity(id, quantity + 1);
  };

  return (
    <div className="flex justify-between mt-6">
      <div className="flex">
        <button
          onClick={handleRemoveButtonClick}
          className="text-gray-600 focus:outline-none mx-2"
        >
          <svg
            className="h-5 w-5"
            fill="none"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
        <img
          className="h-20 w-20 object-cover rounded"
          src={urlFor(mainImage)
            .auto("format")
            .fit("crop")
            .width(750)
            .quality(80)}
          alt=""
        />
        <div className="mx-3">
          <Link href={`/products/${slug.current}`}>
            <a><h3 className="text-sm text-gray-600">{title}</h3></a>
          </Link>
          <div className="flex items-center mt-2">
            <button
              onClick={increaseOrderQuantity}
              className="text-gray-500 focus:outline-none focus:text-gray-600">
              <svg
                className="h-5 w-5"
                fill="none"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
                />
              </svg>
            </button>
            <span className="text-gray-700 mx-2">{quantity}</span>
            <button
              onClick={reduceOrderQuantity}
              className="text-gray-500 focus:outline-none focus:text-gray-600">
              <svg
                className="h-5 w-5"
                fill="none"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
              </svg>
            </button>
          </div>
        </div>
      </div>
      <span className="text-gray-600">${quantity * price}</span>
    </div>
  );
};

export default CartItem;
Enter fullscreen mode Exit fullscreen mode

The cart item provided as a prop to the CartItem is destructured to get its contents. Then we use the useCartContext hook to get helper functions for removing items from the cart and updating the ordered quantity for the cart item. Using the helper functions, we add functionality for the buttons in the component. Clicking on the + icon should increase the number of units to be ordered while clicking the - icon should reduce the number. We also add a button to remove the item entirely from the cart. Finally, we return the JSX for the CartItem component.

The next thing we need to do is fix our Cart component. At the moment, the Cart component is hardcoded to render 3 items. It also takes two props - a cartOpen flag, which determines whether the cart is displayed or collapsed. It also takes a handleOpen function as a prop. This function toggles the cartOpen flag to display or collapse the cart. Since we have a context that manages this for us, we no longer need these props. Update the Cart component to match the following:

// components/Cart.js
import { useCartContext } from "../utils/context";
import CartItem from "./CartItem";

function Cart() {

  const { cartOpen, toggleCartVisibility, cart } = useCartContext();

  const orderTotal = cart.reduce(
    (accumulator, { price, quantity }) => (price * quantity) + accumulator,
    0
  );

    const [phoneNumber, setPhoneNumber] = useState("");

  const updatePhoneNumber = event => {
    setPhoneNumber(event.target.value);
  };

  return (
          <div
          className={`${
                    cartOpen ? "translate-x-0 ease-out" : "translate-x-full ease-in"} 
          fixed right-0 top-0 max-w-xs w-full h-full px-6 py-4 transition 
          duration-300 transform overflow-y-auto 
          bg-white border-l-2 border-gray-300`}
      >
      <div className="flex items-center justify-between">
        <h3 className="text-2xl font-medium text-gray-700">Your cart</h3>
        <button
          onClick={toggleCartVisibility}
          className="text-gray-600 focus:outline-none"
        >
          <svg
            className="h-5 w-5"
            fill="none"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      </div>
      <hr className="my-3" />
      {cart.map(order => <CartItem key={order.id} {...order} />)}
      <div className="mt-8">
        <form className="flex items-center justify-center">
                    <label htmlFor="phoneNumber" className="hidden">Phone Number</label>
          <input
                        id="phoneNumber"
            className="form-input w-48"
            type="text"
                        placeholder="Enter phone number"
            value={phoneNumber}
            onInput={updatePhoneNumber}
          />
        </form>
      </div>
      <hr className="my-3" />
      <span className="text-l font-medium text-gray-700 mr-48">Total</span>
      <span>${orderTotal}</span>
      <a
                    className="flex items-center justify-center mt-4 px-3 py-2 bg-blue-600 
          text-white text-sm uppercase font-medium rounded hover:bg-blue-500 
          focus:outline-none focus:bg-blue-500"
       >
        <span>Checkout</span>
        <svg
          className="h-5 w-5 mx-2"
          fill="none"
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth="2"
          viewBox="0 0 24 24"
          stroke="currentColor"
        >
          <path d="M17 8l4 4m0 0l-4 4m4-4H3" />
        </svg>
      </a>
    </div>
  );
}

export default Cart;
Enter fullscreen mode Exit fullscreen mode

In our newly modified component, we retrieve the cartOpen flag from our useContext hook, along with the items that have been added to the cart and the cartOpen flag. We also calculate the total amount of all the orders in the cart before returning the JSX for the Cart component.

Now that our cart is functional, we can update the cart from the products page. In the components directory, open ProductPage.js and update it to match the following:

// components/ProductPage.js
import { useState } from "react";
import { urlFor, PortableText } from "../utils/sanity";
import { useCartContext } from "../utils/context";

function ProductPage(props) {
  const {
    title,
    defaultProductVariant,
    mainImage,
    body,
    id: productId,
    slug,
  } = props;

  const {
    findOrderInCart,
    addOrderToCart,
    removeOrderFromCart,
    updateOrderQuantity,
    showCart,
    toggleCartVisibility,
  } = useCartContext();

  let orderInCart = findOrderInCart(productId);

  const [count, setCount] = useState(orderInCart?.quantity || 1);

  const handleCount = (value) =>
    !(count === 0 && value === -1) ? setCount(count + value) : count;

  const handleOrderButtonClick = () => {
    if (count === 0 && orderInCart) {
      removeOrderFromCart(productId);
      orderInCart = undefined;
    }
    if (!orderInCart && count > 0) {
      addOrderToCart({
        title,
        slug,
        id: productId,
        price: defaultProductVariant?.price,
        quantity: count,
        mainImage,
      });
    }
    if (orderInCart) {
      updateOrderQuantity(productId, count);
    }
    showCart();
  };

  return (
    <div className="container mx-auto px-6">
      <div className="md:flex md:items-center">
        <div className="w-full h-64 md:w-1/2 lg:h-96">
          <img
            className="h-full w-full rounded-md object-cover max-w-lg mx-auto"
            src={urlFor(mainImage)
              .auto("format")
              .width(1051)
              .fit("crop")
              .quality(80)}
            alt={mainImage?.alt || `Photo of ${title}`}
          />
        </div>
        <div className="w-full max-w-lg mx-auto mt-5 md:ml-8 md:mt-0 md:w-1/2">
          <h3 className="text-gray-700 uppercase text-lg">{title}</h3>
          <span className="text-gray-500 mt-3">
            ${defaultProductVariant?.price}
          </span>
          <hr className="my-3" />
          <div className="mt-2">
            <label className="text-gray-700 text-sm" htmlFor="count">
              Count:
            </label>
            <div className="flex items-center mt-1">
              <button
                onClick={() => handleCount(1)}
                className="text-gray-500 focus:outline-none focus:text-gray-600"
              >
                <svg
                  className="h-5 w-5"
                  fill="none"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                  viewBox="0 0 24 24"
                  stroke="currentColor"
                >
                  <path
                    d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 
                  9 0 11-18 0 9 9 0 0118 0z"
                  />
                </svg>
              </button>
              <span className="text-gray-700 text-lg mx-2">{count}</span>
              <button
                onClick={() => handleCount(-1)}
                className="text-gray-500 focus:outline-none focus:text-gray-600"
              >
                <svg
                  className="h-5 w-5"
                  fill="none"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                  viewBox="0 0 24 24"
                  stroke="currentColor"
                >
                  <path d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
                </svg>
              </button>
            </div>
          </div>
          <div className="flex items-center mt-6">
            <button
              onClick={handleOrderButtonClick}
              className="px-8 py-2 bg-indigo-600 text-white text-sm font-medium
              rounded hover:bg-indigo-500 focus:outline-none focus:bg-indigo-500"
            >
              Order Now
            </button>
            <button
              onClick={toggleCartVisibility}
              className="mx-2 text-gray-600 border rounded-md p-2
              hover:bg-gray-200 focus:outline-none"
            >
              <svg
                className="h-5 w-5"
                fill="none"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293
                2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0
                000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
                />
              </svg>
            </button>
          </div>
        </div>
      </div>
      <div className="mt-16 md:w-2/3">
        <h3 className="text-gray-600 text-2xl font-medium">Description</h3>
        {body && <PortableText blocks={body?.en} className="text-gray-600" />}
      </div>
    </div>
  );
}

export default ProductPage;
Enter fullscreen mode Exit fullscreen mode

On the ProductPage component, we use the helper functions provided by the useCartContext hook to handle the functionality for adding the rendered product to the cart (if it is not already in the cart). Additionally, we can increase or decrease the units we wish to purchase.

On the products page, we see that on each product, there is a button to directly add the product to the cart. At the moment, it doesn't work. Clicking the cart icon also takes us to the product page which we don't really want. To fix that, open components/ProductCard.js and update it to match the following:

// components/ProductCard.js
import Link from "next/link";
import { urlFor } from "../utils/sanity";
import { useCartContext } from "../utils/context";

function ProductCard({ _id, title, mainImage, slug, defaultProductVariant }) {
  const { addOrderToCart } = useCartContext();

  const handleCartButtonClick = () => {
    const order = {
      title,
      slug,
      id: _id,
      price: defaultProductVariant?.price,
      quantity: 1,
      mainImage,
    };
    addOrderToCart(order);
  };

  return (
    <div className="w-full max-w-sm mx-auto rounded-md shadow-md overflow-hidden">
      <div
        className="flex items-end justify-end h-56 w-full bg-cover"
        style={{
          backgroundImage: `url('${urlFor(mainImage)
            .auto("format")
            .fit("crop")
            .width(750)
            .quality(80)}`,
        }}
      >
        <button
          onClick={handleCartButtonClick}
          className="p-2 rounded-full bg-blue-600 text-white mx-5 -mb-4
          hover:bg-blue-500 focus:outline-none focus:bg-blue-500"
        >
          <svg
            className="h-5 w-5"
            fill="none"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293
              2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0
              000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
            />
          </svg>
        </button>
      </div>
      <div className="px-5 py-3">
        <Link href={`/products/${slug?.current}`}>
          <a>
            <h3 className="text-gray-700 uppercase">{title}</h3>
          </a>
        </Link>
        <span className="text-gray-500 mt-2">
          ${defaultProductVariant?.price}
        </span>
      </div>
    </div>
  );
}

export default ProductCard;
Enter fullscreen mode Exit fullscreen mode

Here we use the addOrderToCart function provided by the useCartContext hook to add the product to the cart (or increase the units ordered if it has already been added). We also refactor the JSX such that the user is only taken to the project page by clicking the product title.

In order to see the changes we have made, we need to render the Cart component. If you look at lines 122–124 in components/Layout.js, you'll see that the Cart component is commented out. We can go ahead to uncomment those lines and remove the props that are being passed to the Cart component since those are being handled via the Context API. Update components/Layout.js to match the following code:

omponents/Layout.js
import { useState } from "react";
import Link from "next/link";
import Cart from "./Cart";
import { useCartContext } from "../utils/context";

function Layout({ children }) {
  const [menuOpen, setMenuOpen] = useState(false);
  const handleMenu = () => setMenuOpen(!menuOpen);
  const { toggleCartVisibility } = useCartContext();
  return (
    <div className="bg-white">
      <header>
        <div className="container mx-auto px-6 py-3">
          <div className="flex items-center justify-between">
            <div className="hidden w-full text-gray-600 md:flex md:items-center">
              <svg
                className="h-5 w-5"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  fillRule="evenodd"
                  clipRule="evenodd"
                  d="M16.2721 10.2721C16.2721 12.4813 14.4813 14.2721 12.2721
                  14.2721C10.063 14.2721 8.27214 12.4813 8.27214 10.2721C8.27214
                  8.06298 10.063 6.27212 12.2721 6.27212C14.4813 6.27212 16.2721
                  8.06298 16.2721 10.2721ZM14.2721 10.2721C14.2721 11.3767 13.3767
                  12.2721 12.2721 12.2721C11.1676 12.2721 10.2721 11.3767 10.2721
                  10.2721C10.2721 9.16755 11.1676 8.27212 12.2721 8.27212C13.3767
                  8.27212 14.2721 9.16755 14.2721 10.2721Z"
                  fill="currentColor"
                />
                <path
                  fillRule="evenodd"
                  clipRule="evenodd"
                  d="M5.79417 16.5183C2.19424 13.0909 2.05438 7.39409 5.48178
                  3.79417C8.90918 0.194243 14.6059 0.054383 18.2059
                  3.48178C21.8058 6.90918 21.9457 12.6059 18.5183
                  16.2059L12.3124 22.7241L5.79417 16.5183ZM17.0698
                  14.8268L12.243 19.8965L7.17324 15.0698C4.3733 12.404
                  4.26452 7.97318 6.93028 5.17324C9.59603 2.3733 14.0268 2.26452
                  16.8268 4.93028C19.6267 7.59603 19.7355 12.0268 17.0698 14.8268Z"
                  fill="currentColor"
                />
              </svg>
              <span className="mx-1 text-sm">NY</span>
            </div>
            <div
              className="w-full text-gray-700 md:text-center
              text-2xl font-semibold"
            >
              Pulp Inc.
            </div>
            <div className="flex items-center justify-end w-full">
              <button
                onClick={toggleCartVisibility}
                className="text-gray-600 focus:outline-none mx-4 sm:mx-0"
              >
                <svg
                  className="h-5 w-5"
                  fill="none"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                  viewBox="0 0 24 24"
                  stroke="currentColor"
                >
                  <path
                    d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293
                    2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0
                    000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
                  />
                </svg>
              </button>

              <div className="flex sm:hidden">
                <button
                  onClick={handleMenu}
                  type="button"
                  className="text-gray-600 hover:text-gray-500
                  focus:outline-none focus:text-gray-500"
                  aria-label="toggle menu"
                >
                  <svg viewBox="0 0 24 24" className="h-6 w-6 fill-current">
                    <path
                      fillRule="evenodd"
                      d="M4 5h16a1 1 0 0 1 0 2H4a1 1 0 1 1 0-2zm0 6h16a1 1 0 0 1
                      0 2H4a1 1 0 0 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2z"
                    />
                  </svg>
                </button>
              </div>
            </div>
          </div>
          <nav
            className={`${
              menuOpen ? "" : "hidden"
            } sm:flex sm:justify-center sm:items-center mt-4`}
          >
            <div className="flex flex-col sm:flex-row">
              <Link href="/">
                <a className="mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0">
                  Home
                </a>
              </Link>
              <Link href="/products">
                <a className="mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0">
                  Shop
                </a>
              </Link>
              <Link href="/about">
                <a className="mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0">
                  About
                </a>
              </Link>
            </div>
          </nav>
          <div className="relative mt-6 max-w-lg mx-auto">
            <span className="absolute inset-y-0 left-0 pl-3 flex items-center">
              <svg
                className="h-5 w-5 text-gray-500"
                viewBox="0 0 24 24"
                fill="none"
              >
                <path
                  d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3
                  13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
            </span>

            <input
              className="w-full border rounded-md pl-10 pr-4 py-2
              focus:border-blue-500 focus:outline-none focus:shadow-outline"
              type="text"
              placeholder="Search"
            />
          </div>
        </div>
      </header>

      {/* // This Cart works… well sort of! */}
      <Cart />

      <main className="my-8">{children}</main>
      <footer className="bg-gray-200">
        <div
          className="container mx-auto px-6 py-3 flex
          justify-between items-center"
        >
          <a
            href="#"
            className="text-xl font-bold text-gray-500 hover:text-gray-400"
          >
            Pulp Inc.
          </a>
          <p className="py-2 text-gray-500 sm:py-0">All rights reserved</p>
        </div>
      </footer>
    </div>
  );
}

export default Layout;
Enter fullscreen mode Exit fullscreen mode

Implementing the Checkout Feature

At the moment, clicking on the Checkout button does nothing. For this tutorial, we'll create a simple endpoint that expects a HTTP Post request containing the customer's phone number, the ordered items and the order total. When we send a request to this endpoint, it will send an SMS to the customer and return the content of the message as a response. We can do this in our current project as Next.js provides a solution for us to build APIs with it.

Before we build this endpoint, let's add the Twilio Javascript SDK to our project.

npm install twilio 
Enter fullscreen mode Exit fullscreen mode

This library is only for back end applications running on Node.js.

Next, update the .env file as follows:

# For Twilio Locally
TWILIO_MESSAGING_SID = "your_twilio_messaging_sid"
TWILIO_AUTH_TOKEN = "your_twilio_auth_token"
TWILIO_ACCOUNT_SID = "your_twilio_account_sid"
TWILIO_PHONE_NUMBER = "your_twilio_phone_number"
Enter fullscreen mode Exit fullscreen mode

Next, in the utils directory, create a new file called twilio.js.

touch utils/twilio.js
Enter fullscreen mode Exit fullscreen mode

In the utils/twilio.js file add the following:

utils/twilio.js
const twilio = require("twilio");
const client = new twilio(
  process.env.TWILIO_ACCOUNT_SID,
  process.env.TWILIO_AUTH_TOKEN
);
const twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER;
const messagingServiceSid = process.env.TWILIO_MESSAGING_SID;

const sendSMS = async (recipient, message) => {
  return await client.messages.create({
    body: message,
    to: recipient,
    from: twilioPhoneNumber,
    messagingServiceSid,
  });
};

export default sendSMS;
Enter fullscreen mode Exit fullscreen mode

The sendSMS function will be used by our API to send text messages. It takes two parameters, the recipient's phone number and the message to be sent. In this function, a Twilio client is instantiated using our TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN. We also retrieve our twilio phone number and messaging_sid from the .env file. In addition to the recipient's phone number and message content, these are added to the options for the request to the Twilio rest API.

Next, create the endpoint to handle orders from the front-end. In the pages directory, create a new directory called api.

mkdir pages/api
Enter fullscreen mode Exit fullscreen mode

In the pages/api directory, create a new file called order.js.

touch pages/api/order.js
Enter fullscreen mode Exit fullscreen mode

In pages/api/order.js, add the following code:

// pages/api/order.js
import sendSMS from "../../utils/twilio";

const getMessage = (cart, orderTotal) => {
  const orderId = Math.random().toString(36).substring(2, 9);
  return `Congratulations, your order (${orderId}) worth $${orderTotal}\ 
  for ${cart.length} items has been processed successfully. The items will be \
  delivered within 3 working days.`;
};

const handler = async (request, response) => {
  switch (request.method) {
    case "POST":
      const { cart, orderTotal, phoneNumber } = request.body;
      const message = getMessage(cart, orderTotal);
      await sendSMS(phoneNumber, message);
      response.status(200).json({ message });
      break;
    default:
      response.status(405).end("This method is not allowed for this route.");
  }
};

export default handler;
Enter fullscreen mode Exit fullscreen mode

Here, we declare an async function named handler which will handle the requests made to the API. Since we only want to handle POST requests, we return a 405 response for all other request methods. When a POST request is received, we retrieve the cart (ordered items), the total amount of the order, and the customer's phone number from the request body. Using the sendSMS helper function we created earlier, we send a request to the Twilio API and trigger the dispatch of a text message to the customer's phone number. We use the async/await keyword so that our API does not send the response before the Twilio API responds to our request.

To test this, we'll update our Cart component to make an API request and display the response message in a modal. But before we do that, let's create a component for the modal. Create a file named Modal.js in the components directory.

touch components/Modal.js
Enter fullscreen mode Exit fullscreen mode

In Modal.js, add the following code:

// components/Modal.js
import React from "react";

const Modal = ({ title, message, showModal, closeModal }) => {
  return (
    showModal && (
      <div
        className="absolute inset-0 bg-gray-300 text-gray-900 
      bg-opacity-25 overflow-x-hidden"
      >
        <div
          className="relative px-4 min-h-screen md:flex 
      md:items-center md:justify-center"
        >
          <div
            className="bg-black opacity-25 w-full h-full absolute 
        z-10 inset-0"
          ></div>
          <div
            className="bg-white rounded-lg md:max-w-md md:mx-auto p-4 
          fixed inset-x-0 bottom-0 z-50 mb-4 mx-4 md:relative"
          >
            <div className="md:flex items-center">
              <div
                className="rounded-full border border-gray-300 flex 
              items-center justify-center w-16 h-16 flex-shrink-0 
              mx-auto"
              >
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  className="h-6 w-6"
                  fill="none"
                  viewBox="0 0 24 24"
                  stroke="currentColor"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth={2}
                    d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
                  />
                </svg>
              </div>
              <div
                className="mt-4 md:mt-0 md:ml-6 text-center 
              md:text-left"
              >
                <p className="font-bold">{title}</p>
                <p className="text-sm text-gray-700 mt-1">{message}</p>
              </div>
            </div>
            <div
              className="text-center md:text-right mt-4 md:flex 
            md:justify-end"
            >
              <button
                onClick={closeModal}
                className="flex items-center justify-center mt-4 px-3 
                py-2 bg-blue-600 text-white text-sm uppercase 
                font-medium rounded hover:bg-blue-500 focus:outline-none 
                focus:bg-blue-500"
              >
                Close
              </button>
            </div>
          </div>
        </div>
      </div>
    )
  );
};

export default Modal;
Enter fullscreen mode Exit fullscreen mode

Update the Cart component to match the following:

// components/Cart.js
import { useCartContext } from "../utils/context";
import CartItem from "./CartItem";
import React, { useState } from "react";
import Modal from "./Modal";

function Cart() {
  const {
    cartOpen,
    toggleCartVisibility,
    cart,
    removeAllItemsFromCart,
  } = useCartContext();

  const orderTotal = cart.reduce(
    (accumulator, { price, quantity }) => price * quantity + accumulator,
    0
  );

  const [showModal, setShowModal] = useState(false);

  const [modalTitle, setModalTitle] = useState(null);

  const [modalContent, setModalContent] = useState(null);

  const [phoneNumber, setPhoneNumber] = useState("");

  const updatePhoneNumber = (event) => {
    setPhoneNumber(event.target.value);
  };

  const closeModal = () => {
    removeAllItemsFromCart();
    setPhoneNumber("");
    setShowModal(false);
  };

  const makeOrder = () => {
    fetch("/api/order", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ cart, orderTotal, phoneNumber }),
    })
      .then((response) => response.json())
      .then((data) => {
        setModalContent(data.message);
        setModalTitle("Success");
        setShowModal(true);
      });
  };

  return (
    <>
      <Modal
        title={modalTitle}
        message={modalContent}
        showModal={showModal}
        closeModal={closeModal}
      />
      <div
        className={`${
          cartOpen ? "translate-x-0 ease-out" : "translate-x-full ease-in"
        } 
          fixed right-0 top-0 max-w-xs w-full h-full px-6 py-4 transition 
          duration-300 transform overflow-y-auto 
          bg-white border-l-2 border-gray-300`}
      >
        <div className="flex items-center justify-between">
          <h3 className="text-2xl font-medium text-gray-700">Your cart</h3>
          <button
            onClick={toggleCartVisibility}
            className="text-gray-600 focus:outline-none"
          >
            <svg
              className="h-5 w-5"
              fill="none"
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path d="M6 18L18 6M6 6l12 12" />
            </svg>
          </button>
        </div>
        <hr className="my-3" />
        {cart.map((order) => (
          <CartItem key={order.id} {...order} />
        ))}
        <div className="mt-8">
          <form className="flex items-center justify-center">
            <input
              className="form-input w-48"
              type="text"
              placeholder="Enter phone number"
              value={phoneNumber}
              onInput={updatePhoneNumber}
            />
          </form>
        </div>
        <hr className="my-3" />
        <span className="text-l font-medium text-gray-700 mr-48">Total</span>
        <span>${orderTotal}</span>
        <button
          onClick={makeOrder}
          className="flex items-center justify-center mt-4 px-3 py-2 bg-blue-600
          text-white text-sm uppercase font-medium rounded hover:bg-blue-500
          focus:outline-none focus:bg-blue-500"
        >
          <span>Checkout</span>
          <svg
            className="h-5 w-5 mx-2"
            fill="none"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path d="M17 8l4 4m0 0l-4 4m4-4H3" />
          </svg>
        </button>
      </div>
    </>
  );
}

export default Cart;
Enter fullscreen mode Exit fullscreen mode

Here, we add functionality to the Checkout button by calling the makeOrder function when it is clicked. This function makes a HTTP request to the api/order route which will be handled by the handler function we declared in pages/api/order.js. Upon a successful response, the modal is rendered letting the customer know that the order was processed successfully as shown below.

Success modal shown when 'checkout' button is clicked

An SMS will also be sent to the specified phone number as shown below.

Text message sent via Twilio when 'Checkout' button is clicked

With that, our e-commerce website is ready for launch, Congratulations!!!

Conclusion

In this article, we've seen how programmable messaging can be used to provide a better customer experience by sending notifications in the form of text messages to customers with Twilio SMS as a case study. Using Twilio's JavaScript SDK, we were able to trigger an outbound SMS request to the Twilio API. The API reference as well as code samples in Javascript and other languages (PHP, Python, Java to name a few) can be found here

Discussion (0)