DEV Community

Max Kostinevich
Max Kostinevich

Posted on

1

How to add Social Proof widget to Shopify

In this article I'll show you how to create and add a simple Social Proof / FOMO widget to Shopify store. Instead of using 3rd-party applications, we'll pull the list or recently purchased items using really simple serverless function which we'll host on Cloudflare Workers at no cost.

So, what the Social Proof / FOMO widget is?

I'm pretty sure, you saw these widgets a lot of times on different ecommerce stores.

The purpose of such kind of widgets is to increase conversion rate, credibility and trust. There is a lot of widgets on the market, their functionality and design is pretty similar to each other, and they are pretty expensive (especially if you're just starting your online store).

So, let's make our own Social Proof widget in three simple steps.

Step #1 - Create a new private Shopify App

First, we need to create a private Shopify App which allow us to access our store's data via API. To do this, login to your Shopify Admin Dashboard, and go to Apps → Manage private apps → Create a new private app, fill-in all required fields (such as app name and your email address), and make sure the app is allowed to read Orders information:

After you save the app, you can grab your app credentials (you'll need these credentials in the next step), as shown on the screenshot below:

Step #2 - Create a new Cloudflare Worker

So, what the Cloudflare Worker is? In short, Cloudflare Worker - is a container, which allow you to run a serverless function written in Node.js. "Serverless" means that you do not need to have a server for this function, or even think about configuration of the container - everything is done by Cloudflare for you.

If you haven't worked with Cloudflare before, you need to signup at Cloudflare.com and go to Workers directory.

Then, create a new worker with the following source code:

/*
* Serverless Social Proof widget for Shopify, hosted on Cloudflare Workers.
*
* Learn more at https://maxkostinevich.com/blog/serverless-shopify-social-proof-widget
*
* (c) Max Kostinevich / https://maxkostinevich.com
*/
// Script configuration
const config = {
shopify_app_key: "SHOPIFY_PRIVATE_APP_KEY",
shopify_app_password: "SHOPIFY_PRIVATE_APP_PASSWORD",
shopify_domain: "YOUR_SHOPIFY_STORE.myshopify.com" // Including '.myshopify.com'
};
// --------
// Helper function to return JSON response
const JSONResponse = (message, status = 200) => {
let headers = {
headers: {
"content-type": "application/json;charset=UTF-8",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
},
status: status
};
let response = {
message: message
};
return new Response(JSON.stringify(response), headers);
};
addEventListener("fetch", event => {
const request = event.request;
if (request.method === "OPTIONS") {
event.respondWith(handleOptions(request));
} else {
event.respondWith(handle(request));
}
});
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
};
function handleOptions(request) {
if (
request.headers.get("Origin") !== null &&
request.headers.get("Access-Control-Request-Method") !== null &&
request.headers.get("Access-Control-Request-Headers") !== null
) {
// Handle CORS pre-flight request.
return new Response(null, {
headers: corsHeaders
});
} else {
// Handle standard OPTIONS request.
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, OPTIONS"
}
});
}
}
async function handle(request) {
const shopify_url = `https://${
config.shopify_domain
}/admin/api/2020-01/orders.json`;
return fetch(shopify_url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization:
"Basic " +
btoa(`${config.shopify_app_key}:${config.shopify_app_password}`)
}
})
.then(response => response.json())
.then(data => {
let orders = data.orders;
let filteredOrderData = [];
Object.keys(orders).map(order => {
orders[order].line_items.map(item => {
// It's important to keep the order's financial information private
// Expose only selected information about the purchase,
// like customer first name, city and purchased product name
filteredOrderData.push({
customer_name: orders[order].shipping_address.first_name,
customer_city: orders[order].shipping_address.city,
product_title: item.title
});
});
});
return filteredOrderData;
})
.then(data => {
return JSONResponse(data);
})
.catch(err => {
return JSONResponse("Oops! Something went wrong.", 400);
});
}
view raw worker.js hosted with ❤ by GitHub

And then enter app credentials you created in previous step:

So, what this function does? This function executes each time it receives a GET request, and reads latest 50 orders from your store. As we do not want to expose private order information (such as customer email and other sensitive information), we return filtered data, which includes customer first name, customer city and purchased item. This data will be used by the widget itself.

Step #3 - Add widget to your Shopify Theme

Once you created a Cloudflare Worker, you need to login to your Shopify Admin Dashboard and go to Online Store → Themes → Edit code of the current theme and create a new widget.js file inside of /assets/ directory with the following content:

var fomowidget = {
widgetContainer: "",
widgetInnerContent: "",
closeButton: "",
loop_index: 0,
init: function() {
var self = this;
// Hide widgets on mobile devices
if (self.settings.hideOnMobile && self.isMobile()) {
return true;
}
self.shuffleArray(self.data);
self.appendWidget();
self.eventClose();
setTimeout(function() {
self.rotateWidget();
}, self.settings.intialDelay);
},
// Load widget CSS and HTML
appendWidget: function() {
var self = this;
// CSS
var css = document.createElement("style");
css.innerHTML = self.settings.widgetCss;
document.body.appendChild(css);
// HTML
document.body.innerHTML += self.settings.widgetHtml;
// Get DOM elements
self.widgetContainer = document.getElementById("cp-purchase-notification");
self.widgetInnerContent = document.getElementById("cp-widget-inner");
self.closeButton = document.getElementById("cp-widget-close");
},
// Show widget
showWidget: function() {
var self = this;
self.widgetContainer.classList.remove("fade-out");
self.widgetContainer.classList.add("fade-in");
self.widgetContainer.style.display = "block";
},
// Hide widget
hideWidget: function() {
var self = this;
self.widgetContainer.classList.remove("fade-in");
self.widgetContainer.classList.add("fade-out");
},
// Rotate widget content
rotateWidget: function() {
var self = this;
// update widget content
// @TODO: check if item exists
var data = self.data[self.loop_index];
// @TODO: check if item prop exists
self.widgetInnerContent.innerHTML = `<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDMiIGhlaWdodD0iNDMiIHZpZXdCb3g9IjAgMCA0MyA0MyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjEuNSIgY3k9IjIxLjUiIHI9IjIxLjUiIGZpbGw9IiNFQkUxMDAiIGZpbGwtb3BhY2l0eT0iMC4xNSIvPgo8cGF0aCBkPSJNMjEuNTAwMyA4TDI1LjY3MTUgMTYuODg3N0wzNSAxOC4zMTMzTDI4LjI0OTkgMjUuMjMxMUwyOS44NDMzIDM1TDIxLjUwMDMgMzAuMzg4TDEzLjE1NjcgMzVMMTQuNzUwMSAyNS4yMzExTDggMTguMzEzM0wxNy4zMjg1IDE2Ljg4NzdMMjEuNTAwMyA4WiIgZmlsbD0iI0VGQ0U0QSIvPgo8L3N2Zz4K"/> <p><b>${
data.customer_name
}</b> from <b>${data.customer_city}</b> just bought <b>${
data.product_title
}</b> <small>A few hours ago</small></p>`;
self.showWidget();
// increment loop index
self.loop_index++;
self.loop_index =
self.loop_index >= self.data.length ? 0 : self.loop_index++;
// Hide widget by timeout
setTimeout(function() {
self.hideWidget();
// Schedule next loop
setTimeout(function() {
self.rotateWidget();
}, self.settings.rotateDelay);
}, self.settings.displayLength);
},
// Bind close button
eventClose: function() {
var self = this;
self.closeButton.addEventListener("click", function(e) {
self.hideWidget();
});
},
// Detect mobile device
isMobile: function() {
return (
navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/BlackBerry/i) ||
navigator.userAgent.match(/iPhone|iPad|iPod/i) ||
navigator.userAgent.match(/Opera Mini/i) ||
navigator.userAgent.match(/IEMobile/i) ||
navigator.userAgent.match(/webOS/i)
);
},
// Shuffle array
shuffleArray: function(a) {
var j, x, i;
for (i = a.length; i; i--) {
j = Math.floor(Math.random() * i);
x = a[i - 1];
a[i - 1] = a[j];
a[j] = x;
}
}
};
// Widget settings
fomowidget.settings = {
hideOnMobile: false,
intialDelay: 1000,
displayLength: 2000,
rotateDelay: 4000,
widgetCss:
"@import url(//fonts.googleapis.com/css?family=Raleway:300,700);#cp-purchase-notification{background:#fff;border:0;display:none;border-radius:0;bottom:20px;left:20px;top:auto!important;right:auto!important;padding:0 25px 0 0;position:fixed;text-align:left;width:auto;z-index:99999;font-family:Raleway,sans-serif;-webkit-box-shadow:0 0 4px 0 rgba(0,0,0,.4);-moz-box-shadow:0 0 4px 0 rgba(0,0,0,.4);box-shadow:0 0 4px 0 rgba(0,0,0,.4)}#cp-purchase-notification img{padding-left:5px;padding-top:15px;float:left;max-height:85px;max-width:120px;width:auto}#cp-purchase-notification p{color:#000;float:left;font-size:13px;margin:0 0 0 13px;width:auto;padding:10px 10px 0 0;line-height:20px}#cp-purchase-notification p a{color:#000;display:block;font-size:15px;font-weight:700}#cp-purchase-notification p a:hover{color:#000}#cp-purchase-notification p small{display:block;font-size:10px;margin-bottom:8px}#cp-purchase-notification #cp-widget-close{cursor:pointer;position:absolute;top:10px;right:10px;opacity:.2;background:url(data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhciIgZGF0YS1pY29uPSJ0aW1lcy1jaXJjbGUiIGNsYXNzPSJzdmctaW5saW5lLS1mYSBmYS10aW1lcy1jaXJjbGUgZmEtdy0xNiIgcm9sZT0iaW1nIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0yNTYgOEMxMTkgOCA4IDExOSA4IDI1NnMxMTEgMjQ4IDI0OCAyNDggMjQ4LTExMSAyNDgtMjQ4UzM5MyA4IDI1NiA4em0wIDQ0OGMtMTEwLjUgMC0yMDAtODkuNS0yMDAtMjAwUzE0NS41IDU2IDI1NiA1NnMyMDAgODkuNSAyMDAgMjAwLTg5LjUgMjAwLTIwMCAyMDB6bTEwMS44LTI2Mi4yTDI5NS42IDI1Nmw2Mi4yIDYyLjJjNC43IDQuNyA0LjcgMTIuMyAwIDE3bC0yMi42IDIyLjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwyNTYgMjk1LjZsLTYyLjIgNjIuMmMtNC43IDQuNy0xMi4zIDQuNy0xNyAwbC0yMi42LTIyLjZjLTQuNy00LjctNC43LTEyLjMgMC0xN2w2Mi4yLTYyLjItNjIuMi02Mi4yYy00LjctNC43LTQuNy0xMi4zIDAtMTdsMjIuNi0yMi42YzQuNy00LjcgMTIuMy00LjcgMTcgMGw2Mi4yIDYyLjIgNjIuMi02Mi4yYzQuNy00LjcgMTIuMy00LjcgMTcgMGwyMi42IDIyLjZjNC43IDQuNyA0LjcgMTIuMyAwIDE3eiI+PC9wYXRoPjwvc3ZnPg==);width:16px;height:16px;background-size:cover;progid:DXImageTransform.Microsoft.AlphaImageLoader(src='//s3.eu-west-2.amazonaws.com/fomowidget-static-assets/close.png',sizingMethod='scale')}#cp-purchase-notification #cp-widget-close:hover{opacity:1}@keyframes nFadeIn{from{opacity:0;transform:translate3d(0,100%,0)}to{opacity:1;transform:none}}#cp-purchase-notification.fade-in{opacity:0;animation-name:nFadeIn;animation-duration:1s;animation-fill-mode:both}@keyframes nFadeOut{from{opacity:1}to{opacity:0;transform:translate3d(0,100%,0);bottom:0}}#cp-purchase-notification.fade-out{opacity:0;animation-name:nFadeOut;animation-duration:1s;animation-fill-mode:both}@media screen and (max-width:767px){@keyframes nFadeIn{from{opacity:0;transform:translate3d(0,100%,0)}to{opacity:1;transform:none}}#cp-purchase-notification.fade-in{opacity:0;animation-name:nFadeIn;animation-duration:1s;animation-fill-mode:both}@keyframes nFadeOut{from{opacity:1}to{opacity:0;transform:translate3d(0,100%,0);bottom:0}}#cp-purchase-notification.fade-out{opacity:0;animation-name:nFadeOut;animation-duration:1s;animation-fill-mode:both}#cp-purchase-notification{top:auto!important;right:auto!important;bottom:0!important;left:0!important;width:100%;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;max-width:auto!important;margin-left:0;height:auto;padding:0;text-align:left;border-radius:0}#cp-purchase-notification img{max-width:20%;max-height:auto;position:relative;left:0px;top:0;margin-left:0;margin-right:0;border-radius:0}#cp-purchase-notification p{font-size:11px;width:70%;float:left;margin:0 0 0 13px;padding:10px 10px 0 0}#cp-purchase-notification p a{font-size:13px;height:auto;width:auto;margin-left:0;float:none;padding:0;margin-top:0}}",
widgetHtml:
'<div class=customized id=cp-purchase-notification><div id="cp-widget-inner"></div><span id=cp-widget-close></span></div>'
};
view raw widget.js hosted with ❤ by GitHub

And then, you need to add the following code before closing </body> tag in theme.liquid file:

Do not forget to replace the URL with actual URL of your Cloudflare Worker. If you did everything right, you should see something like this on your Shopify store:


The source code is available on Github Gist and on Codesandbox.

You may take a look at live demo here.

A few notes:

  • For demo purposes I haven't added product images and links to the products to this widget, as this information is not included to Order payload, so it will require additional API request on our serverless function to get all this information.
  • I also haven't included a real time of the purchase, as new orders will be rarely added to the demo store.

This post was originally published on my website.

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more