Skip to main content
All CollectionsIntegrationsTapcart Integration
Adding Rebuy-Powered Custom Blocks to Tapcart Mobile Apps
Adding Rebuy-Powered Custom Blocks to Tapcart Mobile Apps

This help doc outlines how to add a Rebuy Custom Block to your Tapcart mobile app.

Kyle Panganiban avatar
Written by Kyle Panganiban
Updated over 2 weeks ago

What is Tapcart?

Tapcart enables Shopify brands to effortlessly convert online sites into mobile apps and eliminate the need for a developer. By utilizing Tapcart, you can 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 your conversion rate.


Integrating Rebuy and Tapcart

Click the button below to learn how to set up the Rebuy x Tapcart Integration.

The remainder of this help doc outlines how to add Custom Blocks to your Tapcart-powered mobile app to personalize the mobile app experience.


Add Custom Blocks to Tapcart

Disclaimer: Please note that Rebuy does not provide support for any customizations related to Tapcart Custom Blocks. Any modifications or custom code added to these blocks are considered outside the scope of Rebuy’s support services. For further details on our support boundaries and guidelines, please reference our official Support Policy.

If you'd like to add a Custom Block to your Tapcart mobile app, follow these steps to learn how to add GWP offers to your mobile app ⬇️

Custom Blocks Launch Editor

Once the Rebuy x Integration has been configured, navigate to the Tapcart editor in the Shopify Admin by following the steps below:

  1. Click the "Sales Channel" section in the left-hand sidebar in the Shopify Admin.

  2. Select the "Tapcart - Mobile App" option from the dropdown menu.

Once in the Tapcart editor, click the "APP STUDIO" dropdown menu and select the page where you'd like to add your new Custom Block.

Next, click the "Custom" tab and then click the "Launch Blocks Editor" button, which will redirect you to a new page that prompts you to insert HTML, CSS, and JS code.

After you are redirected to the new code page, be sure to name the new Custom Block.

Next, you simply need to copy and paste the three code blocks provided below into their corresponding sections (HTML, CSS, JS) of the Custom Blocks Editor.

See the code blocks below for each of the sections:

HTML Section of the Custom Block Editor

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>

CSS Section of the Custom Block Editor

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%;
}

JS Section of the Custom Block Editor

Paste this code in the "JS" section of the Custom Block Editor (you'll edit this later, 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 / Paste Your API Key

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 and then copy it to your clipboard.

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

Choose a Data Source

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 3 endpoints available for you to select and empower your product recommendations.

This video walks through how to update the Data Source and product recommendations that are being surfaced in your Rebuy Custom Block:

These are the 3 endpoints available for you to select from in the “JS” section of the editor:

  • Custom Data Source

    • Add 2 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

  • Similar Products Endpoint

    • Add 2 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

  • Custom Endpoint

    • Replace the number 52524 const endpoint = "/api/v1/custom/id/52524"; with the number found in the datasource ID itself

    • Example below:

Save and Deploy

Lastly, 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 Tapcart block in the Rebuy Admin?

    • There is no way to change the design of the block in the Rebuy admin. All changes should be made in the Tapcart editor (in the Shopify Admin) via the code found in the Integration editor itself.

    • If you want to do this, we suggest you install a Custom Block. More details can be found here - just look for Rebuy!

Disclaimer: Please note that Rebuy does not provide support for any customizations related to Tapcart Custom Blocks. Any modifications or custom code added to these blocks are considered outside the scope of Rebuy’s support services. For further details on our support boundaries and guidelines, please reference our official Support Policy.

  • What products does the Similar Products Endpoint surface?

    • The Similar Products endpoint looks at product data (tags, types, etc.), specific customer data (cart data, past order data, etc.), and storefront-wide data (general product data, conversion data, etc.) in order to recommend products similar to the one your customer is viewing.

    • The Similar Products endpoint is great for improving Product Discovery at any point of the customer journey.

Did this answer your question?