All Collections
Integrations
Recommendations
Integrating Rebuy with Tapcart
Integrating Rebuy with Tapcart

This article will help you get Rebuy recommendations in your Tapcart app.

Strauss Van Wagenen avatar
Written by Strauss Van Wagenen
Updated over a week ago

What is Tapcart?

Tapcart enables Shopify store owners to convert their online sites into mobile apps effortlessly, eliminating the requirement for a developer. By utilizing Tapcart, you have the ability to construct a personalized shopping app with integrated search capabilities, all without the need for coding. The inclusion of push notifications and a simplified mobile checkout process aids in diminishing abandoned shopping carts and enhancing conversion rates.


Integration Overview

Through Tapcart's Custom Blocks feature, clients have the capability to display Rebuy-powered upsells and cross-sells directly within their mobile shopping app. This integration enables users to utilize AI endpoints or create custom rulesets to effectively promote pertinent product offers and personalize their omnichannel approach.


Installation Instructions

To get started make sure you have Rebuy installed in the Shopify Admin under "Apps" and Tapcart as an available Sales Channel.

Then reach out to your designated Tapcart representative for assistance in enabling the Rebuy integration. They will be able to provide support, address any additional inquiries you may have, and assist you in making any necessary adjustments.


Once you have the Tapcart integration enabled by your Tapcart representative, you can begin to build out the integration! First, to create a Custom Block, go to the Shopify admin and navigate to Tapcart in the "Sales Channels" section.

Click "Custom Blocks" then "Launch Block Editor"

Next, you simply need to copy and paste the three code blocks provided below into their corresponding sections. It's a straightforward process!

Paste this code in the "HTML" section of the Custom Block Editor:

<div
id="rebuy-root"
data-input-product="{{product.id}}"
data-customer="{{customer.id}}"
></div>

Paste this code in the "JS" section of the Custom Block Editor:

You'll edit this later. I promise it'll be easy and you don't need to know how to code!

// Adjust values below per merchant
// Replace with Merchant API Key
const apiKey = "replace with my new api key";

// Recommended AI endpoint
const endpoint = "/api/v1/products/recommended";

// Similar Products AI endpoint
// const endpoint = "/api/v1/products/similar_products";

// Example custom endpoint (add the id of the data source from Rebuy admin)
// const endpoint = "/api/v1/custom/id/52524";

// Set the amount of products that you would like to be returned in the carousel
const productLimit = 8;

// End adjust values

// Template
const source = `
<!-- TITLE: ADJUST AS DESIRED -->
<div class="super-title">
<h3>
Recommended Products
</h3>
</div>

<!-- CAROUSEL CONTAINER -->
<div class="slider">

<!-- EACH CAROUSEL CARD -->
{{#each data}}
<div id="{{carouselSlide}}" class="slide">

<div class="rebuy-container">
<img class="rebuy-image" src="{{image.src}}" alt="">
<div class="rebuy-product-actions">
<div class="rebuy-product-title" onclick="handleProductClick({{id}})">
{{title}}
</div>
<div class="rebuy-price-container">
<span class="rebuy-price">
&#36;{{selected_variant.price}}
</span>
<span class="rebuy-price-compare">
{{#if compare_at_price}}
&#36;{{selected_variant.compare_at_price}}
{{/if}}
</span>
</div>
{{#if (hasVariants variants)}}
<select data-carousel-slide="{{carouselSlide}}" class="rebuy-select"
onchange="handleUpdateSelectedVariant(this)">
{{#each variants}}
<option class="rebuy-product-options" value="{{id}}">{{title}}</option>
{{/each}}
</select>
{{/if}}
<div class="rebuy-add-to-cart">
<button onclick="handleAddToCart(this)" {{#if (outOfStock selected_variant)}} disabled {{/if}}
data-product-id="{{id}}" data-variant-id="{{initialVariant variants}}"
class="rebuy-add-button">
{{#if (outOfStock selected_variant)}}
Out Of Stock
{{else}}
Add To Cart
{{/if}}
</button>
</div>
</div>
</div>
</div>
{{/each}}
</div>

<button class="btn btn-next">
<i class="fa-solid fa-chevron-right" style="color:black !important;"></i>
</button>
<button class="btn btn-prev">
<i class="fa-solid fa-chevron-left" style="color:black !important;"></i>
</button>
`;

const template = Handlebars.compile(source);
const root = document.querySelector("#rebuy-root");
const inputProduct = root.getAttribute("data-input-product");
const customerId = root.getAttribute("data-customer");
let context;
let Rebuy;

function serialize(obj) {
const serialized = [];

const add = (key, value) => {
value = typeof value === "function" ? value() : value;
value = value === null ? "" : value === undefined ? "" : value;
serialized[serialized.length] =
encodeURIComponent(key) + "=" + encodeURIComponent(value);
};

const buildParameters = (prefix, obj) => {
let i, len, key;

if (prefix) {
if (Array.isArray(obj)) {
for (i = 0, len = obj.length; i < len; i++) {
buildParameters(
prefix +
"[" +
(typeof obj[i] === "object" && obj[i] ? i : "") +
"]",
obj[i]
);
}
} else if (Object.prototype.toString.call(obj) === "[object Object]") {
for (key in obj) {
buildParameters(prefix + "[" + key + "]", obj[key]);
}
} else {
add(prefix, obj);
}
} else if (Array.isArray(obj)) {
for (i = 0, len = obj.length; i < len; i++) {
add(obj[i].name, obj[i].value);
}
} else {
for (key in obj) {
buildParameters(key, obj[key]);
}
}

return serialized;
};
return buildParameters("", obj).join("&");
}

const config = {
key: null,
domain: "https://rebuyengine.com",
cdnDomain: "https://cdn.rebuyengine.com",
eventDomain: "https://rebuyengine.com",
};

const makeCall = async (method, path, data, origin) => {
const url = `${origin}${path}`;
const requestUrl = new URL(url);

const requestData = {
key: config.key,
};

if (typeof data == "object" && data != null) {
Object.assign(requestData, data);
}

const requestObject = {
method,
};

if (method == "GET") {
requestUrl.search = serialize(requestData);
} else if (method == "POST") {
requestObject.headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
requestObject.body = new URLSearchParams(requestData);
}

const request = await fetch(requestUrl, requestObject);
return await request.json();
};

// Api class
class Api {
constructor(options) {
if (typeof options == "string") {
config.key = options;
} else if (typeof options == "object" && options != null) {
Object.assign(config, options);
}
}

async callEvent(method, path, data) {
return await makeCall(method, path, data, config.eventDomain);
}

async callCdn(method, path, data) {
return await makeCall(method, path, data, config.cdnDomain);
}

async callApi(method, path, data) {
return await makeCall(method, path, data, config.domain);
}
}

const getProducts = async (productId = "", customer = "") => {
const response = await Rebuy.callApi("GET", endpoint, {
shopify_product_ids: productId,
shopify_customer_id: customer,
limit: productLimit,
});

if (response.data) {
return response;
}
};

function registerHelpers() {
Handlebars.registerHelper("hasVariants", function (value) {
return value.length > 1;
});

Handlebars.registerHelper("initialVariant", function (variants) {
return variants[0].id;
});

Handlebars.registerHelper("outOfStock", function (variant) {
const hasStock = variant.inventory_quantity > 0;
return !hasStock;
});
}

function formatData(info) {
info.data.forEach((product, i, arr) => {
if (i === 0) {
product.previous = `#carousel__slide${arr.length}`;
product.next = `#carousel__slide${i + 2}`;
} else if (i === arr.length - 1) {
product.next = `#carousel__slide${1}`;
product.previous = `#carousel__slide${i}`;
} else {
product.next = `#carousel__slide${i + 2}`;
product.previous = `#carousel__slide${i}`;
}
product.price = product.variants[0].price;
product.compare_at_price = product.variants[0].compare_at_price;
product.carouselSlide = `carousel__slide${i + 1}`;

product.selected_variant = product.variants[0];
});
registerHelpers();
}

function handleProductClick(id) {
const selectedProduct = context.data.find((product) => product.id === id);
const variantId = selectedProduct.selected_variant.id;

Tapcart.actiant.openProduct({ id, variantId });
}

function handleUpdateSelectedVariant(variant) {
const { selectedVariant, productIndex } = getSelectedVariant(variant.value);

const attr = variant.getAttribute("data-carousel-slide");

updateVariantImage(productIndex, selectedVariant, attr);
updateVariantPrice(selectedVariant, attr);
updateAddButton(selectedVariant, variant.value, attr);
}

function getSelectedVariant(id) {
let selectedVariant = {};
let productIndex = 0;
for (let i = 0; i < context.data.length; i++) {
selectedVariant = context.data[i].variants.find((variant) => {
if (parseInt(id) === variant.id) {
return true;
}
});
if (selectedVariant) {
productIndex = i;

break;
}
}
context.data[productIndex].selected_variant = selectedVariant;
return { selectedVariant, productIndex };
}

function selectedVariantHasInvintory(variant) {
return variant.inventory_quantity > 0;
}

function updateVariantImage(productIndex, selectedVariant, attr) {
const productImage = document.querySelector(`#${attr} .rebuy-image`);
const selectedProduct = context.data[productIndex];
let image = "";
if (selectedVariant.image_id) {
image = selectedProduct.images.find(
(img) => img.id == selectedVariant.image_id
);
} else if (selectedProduct.images.length === 1) {
image = selectedProduct.image;
} else if (selectedVariant.option1) {
const option1 = selectedVariant.option1;
const variantWithImageId = selectedProduct.variants.find(
(variant) => variant.option1 === option1 && variant.image_id
);
image = selectedProduct.images.find(
(img) => img.id == variantWithImageId.image_id
);
}

if (image && image.src) {
productImage.setAttribute("src", image.src);
} else {
productImage.setAttribute("src", selectedProduct.image.src);
}
}

function updateVariantPrice(selectedVariant, attr) {
const price = document.querySelector(`#${attr} .rebuy-price`);
const compareAtPrice = document.querySelector(
`#${attr} .rebuy-price-compare`
);

price.textContent = `$${selectedVariant.price}`;
if (selectedVariant.compare_at_price) {
compareAtPrice.textContent = `$${selectedVariant.compare_at_price}`;
} else {
compareAtPrice.textContent = "";
}
}

function updateAddButton(variant, value, attr) {
const hasInvintory = selectedVariantHasInvintory(variant);
const button = document.querySelector(`#${attr} .rebuy-add-button`);

button.setAttribute("data-variant-id", value);

if (hasInvintory) {
button.removeAttribute("disabled");
if (button.textContent.trim() === "Out Of Stock") {
button.textContent = "Add To Cart";
}
} else {
button.setAttribute("disabled", "true");
if (button.textContent.trim() === "Add To Cart") {
button.textContent = "Out Of Stock";
}
}
}

function handleAddToCart(button) {
const productId = button.getAttribute("data-product-id");
const variantId = button.getAttribute("data-variant-id");

const data = {
lineItems: [
{
variantId,
quantity: 1,
attributes: [
{
key: "_source",
value: "Rebuy",
},
{
key: "_attribution",
value: "Rebuy Tapcart Product",
},
],
},
],
};

Tapcart.actions.addToCart(data);
}

function setContainerHeight() {
let maxHeight = 0;
const slides = document.querySelectorAll(".slide");
slides.forEach((slide) => {
if (slide.clientHeight > maxHeight) {
maxHeight = slide.clientHeight;
}
});

if (maxHeight > 200) {
const slider = document.querySelector(".slider");
slider.style.height = maxHeight + "px";
}
}

function initTemplate() {
// Add context to the root div
if (context?.data?.length > 0) {
const html = template(context);
root.innerHTML = html;
}
}

const initSlider = () => {
// Select all slides
const slides = document.querySelectorAll(".slide");

// loop through slides and set each slides translateX property to index * 100%
slides.forEach((slide, indx) => {
slide.style.transform = `translateX(${indx * 100}%)`;
});

// select next slide button
const nextSlide = document.querySelector(".btn-next");

// current slide counter
let curSlide = 0;
// maximum number of slides
let elementmaxSlide = slides.length - 1;
let maxSlide = context.data.length - 1;

// add event listener and navigation functionality
nextSlide.addEventListener("click", function () {
// check if current slide is the last and reset current slide
if (curSlide === maxSlide) {
curSlide = 0;
} else {
curSlide++;
}

// move slide by -100%
slides.forEach((slide, indx) => {
slide.style.transform = `translateX(${100 * (indx - curSlide)}%)`;
});
});

// select prev slide button
const prevSlide = document.querySelector(".btn-prev");

// add event listener and navigation functionality
prevSlide.addEventListener("click", function () {
// check if current slide is the first and reset current slide to last
if (curSlide === 0) {
curSlide = maxSlide;
} else {
curSlide--;
}

// move slide by 100%
slides.forEach((slide, indx) => {
slide.style.transform = `translateX(${100 * (indx - curSlide)}%)`;
});
});
};

// Initalize Rebuy without the before Render function
async function initRebuy() {
Rebuy = new Api(apiKey);
context = await getProducts(inputProduct, customerId);

if (context?.data?.length > 0) {
formatData(context);
initTemplate();
initSlider();
setTimeout(() => {
setContainerHeight();
}, 500);
} else {
const body = document.querySelector("body");
body.style.display = "none";
}
}

initRebuy();

Copy and paste this code in the "CSS" section of the Custom Block Editor:

body {
height: 100vh;
display: grid;
place-items: center;
margin: 0;
}

@font-face {
font-family: tapcartBold;
src: url(https://custom-blocks.tapcart.com/_static/fonts/circular/CircularStd-Bold.otf);
}
@font-face {
font-family: tapcartBook;
src: url(https://custom-blocks.tapcart.com/_static/fonts/circular/CircularStd-Book.otf);
}
@font-face {
font-family: tapcartMedium;
src: url(https://custom-blocks.tapcart.com/_static/fonts/circular/CircularStd-Medium.otf);
}

/* Add to cart button colors can be adjusted here as desired */
.rebuy-add-button {
min-height: 40px;
min-width: 100%;
padding: 10px 15px;
font-size: 15px;
font-family: tapcartBook;
border-radius: 4px;
border-width: 0;
color: #fff;
background: #000;
margin-top: 6px;
}

/* Slider styling */

.slider {
width: 100vw;
max-width: 800px;
height: 550px;
position: relative;
overflow: hidden;
}

.slide {
max-width: 800px;
padding: 16px;
position: absolute;
transition: all 0.5s ease-in-out;
}

.slide .rebuy-container {
width: 100%;
height: 100%;
object-fit: cover;
}

.slide img {
width: 100%;
height: 100%;
object-fit: cover;
}

/* Slider nav buttons */

.btn {
position: absolute;
width: 40px;
height: 40px;
padding: 10px;
border: none;
border-radius: 50%;
z-index: 10px;
cursor: pointer;
background-color: #ffffff82;
font-size: 18px;
}

.btn:active {
transform: scale(1.1);
}

.btn-prev {
top: 7px;
left: 5%;
-webkit-color: black !important;
}

.btn-next {
top: 7px;
right: 5%;
-webkit-color: black !important;
}

/* Rebuy item styling */

/* Title above the carousel */

.super-title {
text-align: center;
font-family: tapcartBook;
font-size: 14px;
}

.rebuy-product-container {
padding: 20px 40px;
}

/* Product info (colors can be passed in here) */

.rebuy-product-title,
.rebuy-price-container,
.rebuy-add-to-cart,
.rebuy-select {
font-size: 15px;
font-family: tapcartMedium;
margin: 4px 0px;
text-align: center;
}

/* Price of item */

.rebuy-price {
font-size: 14px;
color: rgb(75, 75, 75);
}

/* Compare at price if applicable */

.rebuy-price-compare {
color: rgb(39, 39, 39);
text-decoration: line-through;
}

.rebuy-image {
display: block;
max-width: 100%;
margin: 0 auto;
border-radius: 4px;
}

.rebuy-product-actions {
position: relative;
bottom: 0;
}

.rebuy-select {
appearance: none !important;
width: 100%;
padding: 10px;
margin-top: 10px;
font-size: 14px;
border-radius: 4px;
color: black !important;
}

input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none !important;
}

.rebuy-product-options {
width: 100%;
}


After successfully installing those code blocks, proceed to the Rebuy Admin to generate your API key. To accomplish this, follow these steps:

  1. Click on "Account" in the Rebuy Admin.

  2. Select "API Keys" from the menu.

  3. Choose "Create New API Key."

  4. Provide the name "Tapcart X Rebuy Key" for the API key.

Copy and paste the API Key and place it in the "JS" code section of the Custom Block between the quotes on line 3.

Lastly, it is necessary to construct and choose a Data Source that will drive the recommendations. Since we haven't created a widget in the Rebuy admin, there are fewer settings to configure. Currently, there are three endpoints available for you to select and empower the Tapcart X Rebuy recommendations. If you prefer a video tutorial, please refer to the video below for a comprehensive walkthrough.

On lines 6, 9, and 12, you'll notice code referencing different endpoints. These will need to be commented out or not depending on what you'd like to use.

If you'd like to use a custom data source or the Similar products endpoint, you'll add to forward slashes before line 6 so it looks like this: //const endpoint = "/api/v1......"

then remove the "//" from either the Similar products endpoint or custom endpoint on lines 9, and 12. Only one endpoint shouldn't have the "//" slashes on it and show in color like the screenshot above.

In order to use a custom endpoint (datasource) you'll replace the number 52524 const endpoint = "/api/v1/custom/id/52524"; with the number found in the datasource ID itself. Example below:

Save your changes and proceed to deploy the updates to your Tapcart app on the Google Play Store and Apple App Store, following the standard procedure you typically use for any other updates.


FAQs

  • Can I customize the look and style of the widget in the Rebuy admin?

    • Unfortunately there isn't a way to change the design of the widget in the Rebuy admin and all changes will be done in Tapcart via the code found in the Custom Block editor itself.

  • How do I change the # of products being recommended?

  • Check out the video below!
    โ€‹


If you have any questions, feel free to contact our Support team.

Did this answer your question?