
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 aboutReact 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:
- Node Package Manager (NPM)
- Sanity CLI
- AGithub account
- ATwilio account
- A code editor of your choice
Getting started
Setting up Sanity
For this article, we'll be using theNext.js E-commerce official starter to build our web application. To get started, open thestarter page in your browser. A screenshot of the starter page is shown below
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-twiliocdsanity-twilio
This command pulls the repository into a folder namedsanity-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
In the.env
file, replace theSANITY_STUDIO_API_PROJECT_ID
andNEXT_PUBLIC_SANITY_PROJECT_ID
with your sanity project id.
We also need to provide the project details for our sanity studio project. In thestudio
directory, edit theapi
node insanity.json
as follows:
"api":{"projectId":"INSERT_YOUR_PROJECT_ID","dataset":"production"},
Next, install the project dependencies using npm
npminstall
Start the application using NPM.
npm run dev
By default, the application will run on port 3000. Navigate tohttp://localhost:3000/ to see the application.
Next, we need to set up a messaging service on our Twilio account. To do this, head to yourTwilio console and click theCreate Messaging Service
button.
We'll name this serviceSanity E-commerce
and our use case is to notify the users. Complete the form as shown below and clickCreate
.
Next, we need to add a sender to our service. In our case, we need a phone number. ClickAdd Senders
and selectPhone Number
then clickContinue
.
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 canbuy more numbers. Select a phone number and clickAdd Phone Numbers
to complete the set up.
Click on theProperties
menu option to view theMessaging Service SID. Make a copy of this as we will use it to integrate our e-commerce web application with Twilio.
In addition to the Messaging Service ID, you will also need your Twilio phone number,ACCOUNT SID
andAUTH TOKEN
to integrate with Twilio. You can find these on yourdashboard.
As a security precaution, yourAUTH 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 theReact Context API. To get started, create a file namedcontext.js
in theutils
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.
touchutils/context.js
Add the following tocontext.js
// utils/context.jsimport{createContext,useContext,useState}from"react";constCartContext=createContext();constCartContextProvider=({children})=>{const[cart,setCart]=useState([]);const[cartOpen,setCartVisibility]=useState(false);constremoveAllItemsFromCart=()=>{setCart([]);};constshowCart=()=>{setCartVisibility(true);};consthideCart=()=>{setCartVisibility(false);};consttoggleCartVisibility=()=>{setCartVisibility(oldVisibility=>!oldVisibility);};constfindOrderInCart=productId=>cart.find(({id})=>id===productId);constupdateOrderQuantity=(productId,newQuantity)=>{constpredicate=({id})=>id===productId;setCart(oldCart=>{constorder=oldCart.find(predicate);order.quantity=newQuantity;constorderIndex=oldCart.findIndex(predicate);constnewCart=[...oldCart];newCart[orderIndex]=order;returnnewCart;});};constaddOrderToCart=order=>{constorderInCart=findOrderInCart(order.id);if(!orderInCart){setCart(oldCart=>[...oldCart,order]);}else{constnewOrderQuantity=orderInCart.quantity+order.quantity;updateOrderQuantity(order.id,newOrderQuantity);}showCart();};constremoveOrderFromCart=productId=>{setCart(oldCart=>oldCart.filter(({id})=>id!==productId));showCart();};return(<CartContext.Providervalue={{cart,addOrderToCart,findOrderInCart,removeOrderFromCart,updateOrderQuantity,cartOpen,showCart,hideCart,toggleCartVisibility,removeAllItemsFromCart}}>{children}</CartContext.Provider>);};exportconstuseCartContext=()=>{constcontext=useContext(CartContext);if(context===undefined){thrownewError("useCartContext must be used within a CartContextProvider");}returncontext;};exportdefaultCartContextProvider;
Next, we need to wrap the root component of our application with theCartContextProvider
. To do this, openpages/_app.js
and update it to match the following:
// pages/_app.jsimport"../styles/index.css";importLayoutfrom"../components/Layout";importCartContextProviderfrom"../utils/context";functionMyApp({Component,pageProps}){return(<CartContextProvider><Layout><Component{...pageProps}/></Layout></CartContextProvider>);}exportdefaultMyApp;
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:
- The product id.
- The title (name) of the product.
- The slug for the product. This will be used to create a link to the product page.
- The price for a single unit of the product.
- The number of units the customer intends to order.
- The product image.
In thecomponents
directory, create a new file calledCartItem.js
.
touchcomponents/CartItem.js
InCartItem.js
add the following:
// components/CartItem.jsimport{urlFor}from"../utils/sanity";importLinkfrom"next/link";import{useCartContext}from"../utils/context";constCartItem=({id,title,slug,price,quantity,mainImage})=>{const{removeOrderFromCart,updateOrderQuantity}=useCartContext();consthandleRemoveButtonClick=()=>{removeOrderFromCart(id);};constreduceOrderQuantity=()=>{if(quantity>1){updateOrderQuantity(id,quantity-1);}else{removeOrderFromCart(id);}};constincreaseOrderQuantity=()=>{updateOrderQuantity(id,quantity+1);};return(<divclassName="flex justify-between mt-6"><divclassName="flex"><buttononClick={handleRemoveButtonClick}className="text-gray-600 focus:outline-none mx-2"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M6 18L18 6M6 6l12 12"/></svg></button><imgclassName="h-20 w-20 object-cover rounded"src={urlFor(mainImage).auto("format").fit("crop").width(750).quality(80)}alt=""/><divclassName="mx-3"><Linkhref={`/products/${slug.current}`}><a><h3className="text-sm text-gray-600">{title}</h3></a></Link><divclassName="flex items-center mt-2"><buttononClick={increaseOrderQuantity}className="text-gray-500 focus:outline-none focus:text-gray-600"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></button><spanclassName="text-gray-700 mx-2">{quantity}</span><buttononClick={reduceOrderQuantity}className="text-gray-500 focus:outline-none focus:text-gray-600"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></button></div></div></div><spanclassName="text-gray-600">${quantity*price}</span></div>);};exportdefaultCartItem;
The cart item provided as a prop to theCartItem
is destructured to get its contents. Then we use theuseCartContext
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 theCartItem
component.
The next thing we need to do is fix ourCart
component. At the moment, theCart
component is hardcoded to render 3 items. It also takes two props - acartOpen
flag, which determines whether the cart is displayed or collapsed. It also takes ahandleOpen
function as a prop. This function toggles thecartOpen
flag to display or collapse the cart. Since we have a context that manages this for us, we no longer need these props. Update theCart
component to match the following:
// components/Cart.jsimport{useCartContext}from"../utils/context";importCartItemfrom"./CartItem";functionCart(){const{cartOpen,toggleCartVisibility,cart}=useCartContext();constorderTotal=cart.reduce((accumulator,{price,quantity})=>(price*quantity)+accumulator,0);const[phoneNumber,setPhoneNumber]=useState("");constupdatePhoneNumber=event=>{setPhoneNumber(event.target.value);};return(<divclassName={`${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`}><divclassName="flex items-center justify-between"><h3className="text-2xl font-medium text-gray-700">Your cart</h3><buttononClick={toggleCartVisibility}className="text-gray-600 focus:outline-none"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M6 18L18 6M6 6l12 12"/></svg></button></div><hrclassName="my-3"/>{cart.map(order=><CartItemkey={order.id}{...order}/>)}<divclassName="mt-8"><formclassName="flex items-center justify-center"><labelhtmlFor="phoneNumber"className="hidden">Phone Number</label><inputid="phoneNumber"className="form-input w-48"type="text"placeholder="Enter phone number"value={phoneNumber}onInput={updatePhoneNumber}/></form></div><hrclassName="my-3"/><spanclassName="text-l font-medium text-gray-700 mr-48">Total</span><span>${orderTotal}</span><aclassName="flexitems-centerjustify-centermt-4px-3py-2bg-blue-600text-whitetext-smuppercasefont-mediumroundedhover:bg-blue-500focus:outline-nonefocus:bg-blue-500"><span>Checkout</span><svgclassName="h-5 w-5 mx-2"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M17 8l4 4m0 0l-4 4m4-4H3"/></svg></a></div>);}exportdefaultCart;
In our newly modified component, we retrieve thecartOpen
flag from ouruseContext
hook, along with the items that have been added to the cart and thecartOpen
flag. We also calculate the total amount of all the orders in the cart before returning the JSX for theCart
component.
Now that our cart is functional, we can update the cart from the products page. In thecomponents
directory, openProductPage.js
and update it to match the following:
// components/ProductPage.jsimport{useState}from"react";import{urlFor,PortableText}from"../utils/sanity";import{useCartContext}from"../utils/context";functionProductPage(props){const{title,defaultProductVariant,mainImage,body,id:productId,slug,}=props;const{findOrderInCart,addOrderToCart,removeOrderFromCart,updateOrderQuantity,showCart,toggleCartVisibility,}=useCartContext();letorderInCart=findOrderInCart(productId);const[count,setCount]=useState(orderInCart?.quantity||1);consthandleCount=(value)=>!(count===0&&value===-1)?setCount(count+value):count;consthandleOrderButtonClick=()=>{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(<divclassName="container mx-auto px-6"><divclassName="md:flex md:items-center"><divclassName="w-full h-64 md:w-1/2 lg:h-96"><imgclassName="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><divclassName="w-full max-w-lg mx-auto mt-5 md:ml-8 md:mt-0 md:w-1/2"><h3className="text-gray-700 uppercase text-lg">{title}</h3><spanclassName="text-gray-500 mt-3"> ${defaultProductVariant?.price}</span><hrclassName="my-3"/><divclassName="mt-2"><labelclassName="text-gray-700 text-sm"htmlFor="count"> Count:</label><divclassName="flex items-center mt-1"><buttononClick={()=>handleCount(1)}className="text-gray-500 focus:outline-none focus:text-gray-600"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M129v3m00v3m0-3h3m-30H9m120a99011-18099001180z"/></svg></button><spanclassName="text-gray-700 text-lg mx-2">{count}</span><buttononClick={()=>handleCount(-1)}className="text-gray-500 focus:outline-none focus:text-gray-600"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></button></div></div><divclassName="flex items-center mt-6"><buttononClick={handleOrderButtonClick}className="px-8py-2bg-indigo-600text-whitetext-smfont-mediumroundedhover:bg-indigo-500focus:outline-nonefocus:bg-indigo-500"> Order Now</button><buttononClick={toggleCartVisibility}className="mx-2text-gray-600borderrounded-mdp-2hover:bg-gray-200focus:outline-none"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M33h2l.42M713h10l4-8H5.4M713L5.45M713l-2.2932.293c-.63.63-.1841.707.7071.707H17m00a2201004220000-4zm-82a22011-402200140z"/></svg></button></div></div></div><divclassName="mt-16 md:w-2/3"><h3className="text-gray-600 text-2xl font-medium">Description</h3>{body&&<PortableTextblocks={body?.en}className="text-gray-600"/>}</div></div>);}exportdefaultProductPage;
On theProductPage
component, we use the helper functions provided by theuseCartContext
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 theproducts 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, opencomponents/ProductCard.js
and update it to match the following:
// components/ProductCard.jsimportLinkfrom"next/link";import{urlFor}from"../utils/sanity";import{useCartContext}from"../utils/context";functionProductCard({_id,title,mainImage,slug,defaultProductVariant}){const{addOrderToCart}=useCartContext();consthandleCartButtonClick=()=>{constorder={title,slug,id:_id,price:defaultProductVariant?.price,quantity:1,mainImage,};addOrderToCart(order);};return(<divclassName="w-full max-w-sm mx-auto rounded-md shadow-md overflow-hidden"><divclassName="flex items-end justify-end h-56 w-full bg-cover"style={{backgroundImage:`url('${urlFor(mainImage).auto("format").fit("crop").width(750).quality(80)}`,}}><buttononClick={handleCartButtonClick}className="p-2rounded-fullbg-blue-600text-whitemx-5-mb-4hover:bg-blue-500focus:outline-nonefocus:bg-blue-500"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M33h2l.42M713h10l4-8H5.4M713L5.45M713l-2.2932.293c-.63.63-.1841.707.7071.707H17m00a2201004220000-4zm-82a22011-402200140z"/></svg></button></div><divclassName="px-5 py-3"><Linkhref={`/products/${slug?.current}`}><a><h3className="text-gray-700 uppercase">{title}</h3></a></Link><spanclassName="text-gray-500 mt-2"> ${defaultProductVariant?.price}</span></div></div>);}exportdefaultProductCard;
Here we use theaddOrderToCart
function provided by theuseCartContext
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 theCart
component. If you look at lines 122–124 incomponents/Layout.js
, you'll see that theCart
component is commented out. We can go ahead to uncomment those lines and remove the props that are being passed to theCart
component since those are being handled via the Context API. Updatecomponents/Layout.js
to match the following code:
omponents/Layout.jsimport{useState}from"react";importLinkfrom"next/link";importCartfrom"./Cart";import{useCartContext}from"../utils/context";functionLayout({children}){const[menuOpen,setMenuOpen]=useState(false);consthandleMenu=()=>setMenuOpen(!menuOpen);const{toggleCartVisibility}=useCartContext();return(<divclassName="bg-white"><header><divclassName="container mx-auto px-6 py-3"><divclassName="flex items-center justify-between"><divclassName="hidden w-full text-gray-600 md:flex md:items-center"><svgclassName="h-5 w-5"viewBox="0 0 24 24"fill="none"xmlns="http://www.w3.org/2000/svg"><pathfillRule="evenodd"clipRule="evenodd"d="M16.272110.2721C16.272112.481314.481314.272112.272114.2721C10.06314.27218.2721412.48138.2721410.2721C8.272148.0629810.0636.2721212.27216.27212C14.48136.2721216.27218.0629816.272110.2721ZM14.272110.2721C14.272111.376713.376712.272112.272112.2721C11.167612.272110.272111.376710.272110.2721C10.27219.1675511.16768.2721212.27218.27212C13.37678.2721214.27219.1675514.272110.2721Z"fill="currentColor"/><pathfillRule="evenodd"clipRule="evenodd"d="M5.7941716.5183C2.1942413.09092.054387.394095.481783.79417C8.909180.19424314.60590.05438318.20593.48178C21.80586.9091821.945712.605918.518316.2059L12.312422.7241L5.7941716.5183ZM17.069814.8268L12.24319.8965L7.1732415.0698C4.373312.4044.264527.973186.930285.17324C9.596032.373314.02682.2645216.82684.93028C19.62677.5960319.735512.026817.069814.8268Z"fill="currentColor"/></svg><spanclassName="mx-1 text-sm">NY</span></div><divclassName="w-fulltext-gray-700md:text-centertext-2xlfont-semibold"> Pulp Inc.</div><divclassName="flex items-center justify-end w-full"><buttononClick={toggleCartVisibility}className="text-gray-600 focus:outline-none mx-4 sm:mx-0"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M33h2l.42M713h10l4-8H5.4M713L5.45M713l-2.2932.293c-.63.63-.1841.707.7071.707H17m00a2201004220000-4zm-82a22011-402200140z"/></svg></button><divclassName="flex sm:hidden"><buttononClick={handleMenu}type="button"className="text-gray-600hover:text-gray-500focus:outline-nonefocus:text-gray-500"aria-label="toggle menu"><svgviewBox="0 0 24 24"className="h-6 w-6 fill-current"><pathfillRule="evenodd"d="M45h16a1100102H4a110110-2zm06h16a1100102H4a110010-2zm06h16a1100102H4a110010-2z"/></svg></button></div></div></div><navclassName={`${menuOpen?"":"hidden"} sm:flex sm:justify-center sm:items-center mt-4`}><divclassName="flex flex-col sm:flex-row"><Linkhref="/"><aclassName="mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0"> Home</a></Link><Linkhref="/products"><aclassName="mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0"> Shop</a></Link><Linkhref="/about"><aclassName="mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0"> About</a></Link></div></nav><divclassName="relative mt-6 max-w-lg mx-auto"><spanclassName="absolute inset-y-0 left-0 pl-3 flex items-center"><svgclassName="h-5 w-5 text-gray-500"viewBox="0 0 24 24"fill="none"><pathd="M2121L1515M1710C1713.86613.866171017C6.1340117313.866310C36.134016.134013103C13.8663176.134011710Z"stroke="currentColor"strokeWidth="2"strokeLinecap="round"strokeLinejoin="round"/></svg></span><inputclassName="w-fullborderrounded-mdpl-10pr-4py-2focus:border-blue-500focus:outline-nonefocus:shadow-outline"type="text"placeholder="Search"/></div></div></header>{/* // This Cart works… well sort of! */}<Cart/><mainclassName="my-8">{children}</main><footerclassName="bg-gray-200"><divclassName="containermx-autopx-6py-3flexjustify-betweenitems-center"><ahref="#"className="text-xl font-bold text-gray-500 hover:text-gray-400"> Pulp Inc.</a><pclassName="py-2 text-gray-500 sm:py-0">All rights reserved</p></div></footer></div>);}exportdefaultLayout;
Implementing the Checkout Feature
At the moment, clicking on theCheckout
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 asolution for us to build APIs with it.
Before we build this endpoint, let's add the Twilio Javascript SDK to our project.
npminstalltwilio
This library is only for back end applications running on Node.js.
Next, update the.env
file as follows:
# For Twilio LocallyTWILIO_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"
Next, in theutils
directory, create a new file calledtwilio.js
.
touchutils/twilio.js
In theutils/twilio.js
file add the following:
utils/twilio.jsconsttwilio=require("twilio");constclient=newtwilio(process.env.TWILIO_ACCOUNT_SID,process.env.TWILIO_AUTH_TOKEN);consttwilioPhoneNumber=process.env.TWILIO_PHONE_NUMBER;constmessagingServiceSid=process.env.TWILIO_MESSAGING_SID;constsendSMS=async(recipient,message)=>{returnawaitclient.messages.create({body:message,to:recipient,from:twilioPhoneNumber,messagingServiceSid,});};exportdefaultsendSMS;
ThesendSMS
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 ourTWILIO_ACCOUNT_SID
andTWILIO_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 thepages
directory, create a new directory calledapi
.
mkdirpages/api
In thepages/api
directory, create a new file calledorder.js
.
touchpages/api/order.js
Inpages/api/order.js
, add the following code:
// pages/api/order.jsimportsendSMSfrom"../../utils/twilio";constgetMessage=(cart,orderTotal)=>{constorderId=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.`;};consthandler=async(request,response)=>{switch(request.method){case"POST":const{cart,orderTotal,phoneNumber}=request.body;constmessage=getMessage(cart,orderTotal);awaitsendSMS(phoneNumber,message);response.status(200).json({message});break;default:response.status(405).end("This method is not allowed for this route.");}};exportdefaulthandler;
Here, we declare an async function namedhandler
which will handle the requests made to the API. Since we only want to handlePOST
requests, we return a405
response for all other request methods. When aPOST
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 thesendSMS
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 theasync/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 ourCart
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 namedModal.js
in thecomponents
directory.
touchcomponents/Modal.js
InModal.js
, add the following code:
// components/Modal.jsimportReactfrom"react";constModal=({title,message,showModal,closeModal})=>{return(showModal&&(<divclassName="absoluteinset-0bg-gray-300text-gray-900bg-opacity-25overflow-x-hidden"><divclassName="relativepx-4min-h-screenmd:flexmd:items-centermd:justify-center"><divclassName="bg-blackopacity-25w-fullh-fullabsolutez-10inset-0"></div><divclassName="bg-whiterounded-lgmd:max-w-mdmd:mx-autop-4fixedinset-x-0bottom-0z-50mb-4mx-4md:relative"><divclassName="md:flex items-center"><divclassName="rounded-fullborderborder-gray-300flexitems-centerjustify-centerw-16h-16flex-shrink-0mx-auto"><svgxmlns="http://www.w3.org/2000/svg"className="h-6 w-6"fill="none"viewBox="0 0 24 24"stroke="currentColor"><pathstrokeLinecap="round"strokeLinejoin="round"strokeWidth={2}d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></div><divclassName="mt-4md:mt-0md:ml-6text-centermd:text-left"><pclassName="font-bold">{title}</p><pclassName="text-sm text-gray-700 mt-1">{message}</p></div></div><divclassName="text-centermd:text-rightmt-4md:flexmd:justify-end"><buttononClick={closeModal}className="flexitems-centerjustify-centermt-4px-3py-2bg-blue-600text-whitetext-smuppercasefont-mediumroundedhover:bg-blue-500focus:outline-nonefocus:bg-blue-500"> Close</button></div></div></div></div>));};exportdefaultModal;
Update theCart
component to match the following:
// components/Cart.jsimport{useCartContext}from"../utils/context";importCartItemfrom"./CartItem";importReact,{useState}from"react";importModalfrom"./Modal";functionCart(){const{cartOpen,toggleCartVisibility,cart,removeAllItemsFromCart,}=useCartContext();constorderTotal=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("");constupdatePhoneNumber=(event)=>{setPhoneNumber(event.target.value);};constcloseModal=()=>{removeAllItemsFromCart();setPhoneNumber("");setShowModal(false);};constmakeOrder=()=>{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(<><Modaltitle={modalTitle}message={modalContent}showModal={showModal}closeModal={closeModal}/><divclassName={`${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`}><divclassName="flex items-center justify-between"><h3className="text-2xl font-medium text-gray-700">Your cart</h3><buttononClick={toggleCartVisibility}className="text-gray-600 focus:outline-none"><svgclassName="h-5 w-5"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M6 18L18 6M6 6l12 12"/></svg></button></div><hrclassName="my-3"/>{cart.map((order)=>(<CartItemkey={order.id}{...order}/>))}<divclassName="mt-8"><formclassName="flex items-center justify-center"><inputclassName="form-input w-48"type="text"placeholder="Enter phone number"value={phoneNumber}onInput={updatePhoneNumber}/></form></div><hrclassName="my-3"/><spanclassName="text-l font-medium text-gray-700 mr-48">Total</span><span>${orderTotal}</span><buttononClick={makeOrder}className="flexitems-centerjustify-centermt-4px-3py-2bg-blue-600text-whitetext-smuppercasefont-mediumroundedhover:bg-blue-500focus:outline-nonefocus:bg-blue-500"><span>Checkout</span><svgclassName="h-5 w-5 mx-2"fill="none"strokeLinecap="round"strokeLinejoin="round"strokeWidth="2"viewBox="0 0 24 24"stroke="currentColor"><pathd="M17 8l4 4m0 0l-4 4m4-4H3"/></svg></button></div></>);}exportdefaultCart;
Here, we add functionality to theCheckout
button by calling themakeOrder
function when it is clicked. This function makes a HTTP request to theapi/order
route which will be handled by thehandler
function we declared inpages/api/order.js
. Upon a successful response, the modal is rendered letting the customer know that the order was processed successfully as shown below.
An SMS will also be sent to the specified phone number as shown below.
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 foundhere
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse