Display the cashback a customer will earn on their cart
Show estimated cashback in the cart
You can display an estimated cashback amount in your Shopify cart by combining the customer's Rivo cashback opt-in with the active offer and order settings published to your storefront.
The cart amount is an estimate. Rivo determines the final cashback after the order is placed and all eligibility rules can be checked.
Before you begin
Customers must first opt in through a Rivo cashback link containing:
?rivo-cashback=<offer-identifier>Rivo then saves their cashback profile in:
localStorage["rivo_cashback_profile"]The exact key is rivo_cashback_profile, rather than rivo_cashback.
Rivo also attaches the following attributes to the customer's Shopify cart:
_rivo_cashback_offer
_rivo_cashback_opt_in_idUse both the saved profile and cart attributes to avoid displaying cashback for an expired or incomplete opt-in.
Cashback data available on the storefront
The cashback program configuration is available through window.Rivo.loy_config.
| Information | Location |
|---|---|
| Whether cashback is enabled | cashback_program_enabled |
| Active cashback offers | cashback_offers |
| Excluded products and collections | order_settings |
| Discount, tax, and shipping behavior | order_settings |
| Cashback rounding rules | order_settings |
The selected offer's earning_rule contains the earning amount:
| Setting | Purpose |
|---|---|
points_type | multiplier for percentage-based cashback or fixed for a flat amount |
balance_amount | The cashback amount earned |
currency_base_amount | The amount of spend represented by one earning increment |
For example, these settings represent 30% cashback:
{
"points_type": "multiplier",
"balance_amount": 0.3,
"currency_base_amount": 1
}Add a cashback message to the cart
Add a placeholder to your cart template:
<p id="rivo-cashback-estimate" hidden></p>Then add the following JavaScript after Rivo's storefront script has loaded:
function getRivoCashbackProfile() {
try {
return JSON.parse(
localStorage.getItem("rivo_cashback_profile") || "{}"
);
} catch (error) {
return {};
}
}
function applyCashbackRounding(amount, settings) {
// Percentage earnings are first calculated to two decimal places.
let roundedAmount = Math.round(amount * 100) / 100;
if (!settings.credits_order_earnings_rounding_enabled) {
return roundedAmount;
}
const precision =
settings.credits_order_earnings_rounding_precision ?? 2;
const factor = 10 ** precision;
switch (settings.credits_order_earnings_rounding_type) {
case "floor":
return Math.floor(roundedAmount * factor) / factor;
case "ceil":
return Math.ceil(roundedAmount * factor) / factor;
default:
return Math.round(roundedAmount * factor) / factor;
}
}
function formatCashback(amount, cart) {
const currency =
cart.currency ||
window.Shopify?.currency?.active;
if (!currency) {
return amount.toFixed(2);
}
return new Intl.NumberFormat(
document.documentElement.lang || "en",
{
style: "currency",
currency
}
).format(amount);
}
async function estimateRivoCashback() {
const config = window.Rivo?.loy_config;
if (!config?.cashback_program_enabled) {
return null;
}
const profile = getRivoCashbackProfile();
if (!profile.cashback_offer || !profile.opt_in_id) {
return null;
}
const cartResponse = await fetch("/cart.js");
if (!cartResponse.ok) {
return null;
}
const cart = await cartResponse.json();
const attributes = cart.attributes || {};
// Confirm that the saved opt-in is attached to the current cart.
if (
attributes._rivo_cashback_offer !== profile.cashback_offer ||
String(attributes._rivo_cashback_opt_in_id) !==
String(profile.opt_in_id)
) {
return null;
}
const offer = (config.cashback_offers || []).find(
(candidate) =>
candidate.identifier === profile.cashback_offer &&
candidate.status === "active" &&
candidate.earning_rule?.status === "active"
);
if (!offer) {
return null;
}
const rule = offer.earning_rule;
const settings = config.order_settings || {};
const excludedProductIds = new Set(
(settings.points_program_excluded_product_ids || []).map(String)
);
const skipOrderProductIds = new Set(
(settings.skip_order_earnings_on_product_ids || []).map(String)
);
// These products prevent the entire order from earning cashback.
const shouldSkipOrder = cart.items.some((item) =>
skipOrderProductIds.has(String(item.product_id))
);
if (shouldSkipOrder) {
return { amount: 0, cart };
}
if (rule.points_type === "fixed") {
const amount = applyCashbackRounding(
Number(rule.balance_amount || 0),
settings
);
return { amount, cart };
}
if (
rule.points_type !== "multiplier" ||
Number(rule.currency_base_amount) <= 0
) {
return null;
}
const eligibleSubtotalInCents = cart.items
.filter(
(item) =>
!excludedProductIds.has(String(item.product_id))
)
.reduce((total, item) => {
/*
* Despite its name, `order_price_exclude_discounts`
* means that total savings are added back when true.
*/
const linePrice =
settings.order_price_exclude_discounts
? item.original_line_price
: item.final_line_price;
return total + linePrice;
}, 0);
const eligibleSubtotal = eligibleSubtotalInCents / 100;
const amount = applyCashbackRounding(
Number(rule.balance_amount) *
(eligibleSubtotal / Number(rule.currency_base_amount)),
settings
);
return { amount, cart };
}
async function updateRivoCashbackMessage() {
const element = document.querySelector(
"#rivo-cashback-estimate"
);
if (!element) return;
const estimate = await estimateRivoCashback();
if (!estimate || estimate.amount <= 0) {
element.hidden = true;
element.textContent = "";
return;
}
const formattedAmount = formatCashback(
estimate.amount,
estimate.cart
);
element.textContent =
`You'll earn an estimated ${formattedAmount} back on this order!`;
element.hidden = false;
}
updateRivoCashbackMessage();If your theme updates the cart without reloading the page, call updateRivoCashbackMessage() again from your theme's cart-updated callback.
How exclusions are handled
The example supports two product exclusion settings:
-
points_program_excluded_product_idsremoves matching products from the eligible subtotal. -
skip_order_earnings_on_product_idsmakes the entire cart ineligible when it contains a matching product.
Product IDs are converted to strings before comparison so the calculation works whether the published IDs are numbers or strings.
Limitations of a cart estimate
Some eligibility rules cannot be confirmed reliably from /cart.js:
-
Excluded collections: The cart does not include every collection to which a product belongs.
-
Order tags: These are not available before the order is created.
-
Taxes and shipping: Their final amounts may not be known on the cart page.
-
Customer eligibility: New-customer requirements, previous cashback earnings, and offer audience eligibility are confirmed later.
-
Campaign multipliers: Collection-based or other conditional multipliers may require additional product information.
For these reasons, describe the displayed amount as "estimated" or "potential" cashback. The amount awarded by Rivo after checkout remains authoritative.
Updated 16 days ago