Skip to main content

Controlling Carts for Local Pickup and Delivery

Show customers whether pickup or delivery is available. Prevent customers from placing orders that do not match your fulfillment rules for stores that separate products by location, route, region, or collection.

RP Local Pickup & Delivery Help Article

Overview

RP Local Pickup & Delivery helps Shopify merchants offer local pickup and local delivery options to customers during the buying journey. It is designed for stores that serve specific towns, regions, service areas, or collection-based product groups where fulfillment options need to be controlled before checkout.

The widget can be used to:

  • Show customers whether pickup or delivery is available.

  • Prevent customers from placing orders that do not match your fulfillment rules.

  • Keep local pickup, delivery, and shipping experiences clear and consistent.

  • Support stores that separate products by location, route, region, or collection.

What the widget does

RP Local Pickup & Delivery adds a storefront layer that helps guide customers before they reach checkout. Depending on your store setup, it can display pickup and delivery information, validate customer selections, and prevent checkout when cart contents do not meet your rules.

Common use cases include:

  • A store offering pickup from one or more physical locations.

  • A business delivering only to selected local areas.

  • A merchant selling products that belong to separate pickup or delivery collections.

  • A store that needs to prevent products from different fulfillment groups being ordered together.

Before you begin

Before installing the widget, decide how your local fulfillment rules should work.

You should confirm:

  • Which products are eligible for pickup.

  • Which products are eligible for local delivery.

  • Whether pickup and delivery products can be mixed in the same cart.

  • Which collections represent each pickup, delivery, location, or fulfillment group.

  • What message customers should see if their cart does not qualify.

Recommended Shopify setup

1. Create fulfillment collections

In Shopify Admin, go to Products > Collections and create collections for your local fulfillment rules.

Examples:

  • Local Pickup

  • Local Delivery

  • North Route Delivery

  • South Route Delivery

  • Store A Pickup

  • Store B Pickup

Add the relevant products to each collection.

2. Decide whether mixed collections are allowed

For most local pickup and delivery setups, we recommend keeping each order limited to one fulfillment group.

For example, a customer should not be able to check out with:

  • One item from Store A Pickup

  • One item from Store B Pickup

Or:

  • One item from North Route Delivery

  • One item from South Route Delivery

This helps avoid fulfillment confusion and prevents customers from selecting an invalid delivery or pickup option.

How to install on your Shopify store

Step 1: Add the widget script

Add the RP Local Pickup & Delivery widget script to your Shopify theme.

In Shopify Admin:

  1. Go to Online Store > Themes.

  2. Click ... > Edit code on your active theme.

  3. Open theme.liquid.

  4. Add the widget script before the closing </body> tag.

Example:

<script src="{{ 'rp-local-pickup-delivery.js' | asset_url }}" defer></script>

If the script is hosted externally, use the hosted script URL provided with your RP widget package.

Step 2: Add product collection group data

To allow the widget to validate cart rules, each product form should include the product’s pickup or delivery group.

Example:

<form action="/cart/add" method="post" data-rp-collection-group="local-pickup">   <!-- product form content --> </form>

For delivery-specific products:

<form action="/cart/add" method="post" data-rp-collection-group="local-delivery">   <!-- product form content --> </form>

For route-based delivery:

<form action="/cart/add" method="post" data-rp-collection-group="north-route-delivery">   <!-- product form content --> </form>

Step 3: Store the collection group as a cart line property

For the most reliable validation, add the group as a hidden line item property inside the product form.

Example:

<input type="hidden" name="properties[_rp_collection_group]" value="local-pickup">

A full product form example:

<form action="/cart/add" method="post" data-rp-collection-group="local-pickup">   <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">   <input type="hidden" name="properties[_rp_collection_group]" value="local-pickup">   <button type="submit">Add to cart</button> </form>

Step 4: Add a customer-facing warning area

Add a warning container where the widget can display pickup or delivery rule messages.

<div data-rp-local-pickup-delivery-warning hidden></div>

You can place this near the cart drawer, cart page checkout button, or product form.

Step 5: Configure the widget

Add configuration before the widget script loads.

<script>   window.RPLocalPickupDelivery = {     enabled: true,     preventMixedCollections: true,     messages: {       addBlocked: "This item cannot be added because your cart already contains an item from another pickup or delivery group.",       checkoutBlocked: "Your cart contains items from different pickup or delivery groups. Please keep one pickup or delivery group per order."     }   }; </script>

Then load the widget script after the configuration.

<script src="{{ 'rp-local-pickup-delivery.js' | asset_url }}" defer></script>

Example implementation

Use this example when you want to prevent checkout if customers mix products from different local fulfillment groups.

<script>   window.RPLocalPickupDelivery = {     enabled: true,     preventMixedCollections: true,     selectors: {       checkoutButtons: '[name="checkout"], [href="/checkout"]',       warningContainer: '[data-rp-local-pickup-delivery-warning]'     },     messages: {       addBlocked: "Please complete your current order before adding products from another pickup or delivery group.",       checkoutBlocked: "Your cart includes products from more than one pickup or delivery group. Please remove one group before checkout."     }   }; </script>  <div data-rp-local-pickup-delivery-warning hidden></div> <script src="{{ 'rp-local-pickup-delivery.js' | asset_url }}" defer></script>

Customer experience

When the widget is active, customers will be guided before checkout.

If the cart is valid, the customer can continue normally.

If the customer tries to add an item from a different pickup or delivery group, the widget shows a message and prevents the item from being added.

If the cart already contains mixed groups, the widget disables or blocks checkout and tells the customer how to fix the cart.

Testing checklist

After installing the widget, test the following scenarios:

  • Add one pickup product to the cart.

  • Add another product from the same pickup group.

  • Try to add a product from a different pickup group.

  • Try to mix a pickup product and a delivery product.

  • Try to continue to checkout with a valid cart.

  • Try to continue to checkout with an invalid mixed cart.

  • Test on desktop and mobile.

  • Test with the cart drawer, cart page, and product page.

Troubleshooting

The widget is not blocking checkout

Confirm that the checkout button selector matches your theme.

Common checkout selectors include:

[name="checkout"] [href="/checkout"] button[type="submit"]

The widget cannot detect product groups

Confirm that your product forms include data-rp-collection-group and the hidden cart line property.

<form data-rp-collection-group="local-pickup">   <input type="hidden" name="properties[_rp_collection_group]" value="local-pickup"> </form>

The warning message does not appear

Confirm that the warning container exists on the page.

<div data-rp-local-pickup-delivery-warning hidden></div>

Customers can still bypass the rule

Storefront widgets guide and block the customer experience before checkout. For stronger enforcement, pair the widget with Shopify Functions, checkout validation, or server-side order validation where available.

Best practices

  • Use clear collection names that match your fulfillment operations.

  • Keep one pickup or delivery group per order unless your team can fulfill mixed orders.

  • Show customer-facing messages before checkout, not only after an invalid action.

  • Test every theme template that allows adding products to the cart.

  • Re-test after changing your Shopify theme, cart drawer, or checkout buttons.

Support

For setup help, contact Rose Perl Technology with the following details:

  • Shopify store URL

  • Theme name

  • Product or collection examples

  • Pickup and delivery rules

  • Screenshot or screen recording of the issue

Full Script for Easiser cut and paste.

/*
* Mixed Collection Checkout Guard
* Prevents customers from adding products from different collection groups to the same Shopify cart,
* and blocks checkout if an already-mixed cart is detected.
*
* Install:
* 1) Upload this file as an asset or include it through your widget bundle.
* 2) Add collection data to product forms/buttons when possible:
* <form action="/cart/add" data-rp-collection-group="store-locator">...</form>
* or:
* <button data-product-handle="example-product" data-rp-collection-group="store-locator">Add</button>
* 3) Optionally configure handle-to-group mappings before loading the script:
* window.RPMixedCollectionGuard = {
* enabled: true,
* productGroups: {
* "example-product": "store-locator",
* "another-product": "pickup-delivery"
* }
* };
*/

(function () {
"use strict";

const DEFAULTS = {
enabled: true,
debug: false,
productGroups: {},
selectors: {
addToCartForms: 'form[action*="/cart/add"]',
checkoutButtons: '[name="checkout"], [href="/checkout"], form[action*="/checkout"] button[type="submit"], button[data-testid="Checkout-button"]',
warningContainer: '[data-rp-mixed-collection-warning]'
},
messages: {
addBlocked: "This product cannot be added because your cart already contains an item from another collection. Please complete or clear your cart first.",
checkoutBlocked: "Your cart contains products from different collections. Please keep items from one collection per order before checking out."
},
onBlocked: null
};

const config = merge(DEFAULTS, window.RPMixedCollectionGuard || {});
if (!config.enabled) return;

const state = {
cart: null,
cartGroups: new Set(),
initialized: false
};

init();

async function init() {
await refreshCartState();
bindAddToCartGuards();
bindCheckoutGuards();
updateCheckoutState();
state.initialized = true;

document.addEventListener("cart:updated", async () => {
await refreshCartState();
updateCheckoutState();
});
}

function bindAddToCartGuards() {
document.addEventListener("submit", async function (event) {
const form = event.target.closest(config.selectors.addToCartForms);
if (!form) return;

const incomingGroup = getGroupFromElement(form);
if (!incomingGroup) return;

await refreshCartState();
if (wouldMixGroups(incomingGroup)) {
event.preventDefault();
event.stopPropagation();
showBlocked(config.messages.addBlocked, form);
}
}, true);

document.addEventListener("click", async function (event) {
const trigger = event.target.closest("[data-product-handle], [data-rp-collection-group]");
if (!trigger || trigger.closest(config.selectors.addToCartForms)) return;

const incomingGroup = getGroupFromElement(trigger);
if (!incomingGroup) return;

await refreshCartState();
if (wouldMixGroups(incomingGroup)) {
event.preventDefault();
event.stopPropagation();
showBlocked(config.messages.addBlocked, trigger);
}
}, true);
}

function bindCheckoutGuards() {
document.addEventListener("click", async function (event) {
const checkoutTrigger = event.target.closest(config.selectors.checkoutButtons);
if (!checkoutTrigger) return;

await refreshCartState();
if (state.cartGroups.size > 1) {
event.preventDefault();
event.stopPropagation();
showBlocked(config.messages.checkoutBlocked, checkoutTrigger);
updateCheckoutState();
}
}, true);

document.addEventListener("submit", async function (event) {
const form = event.target;
const isCheckoutForm = form && form.matches('form[action*="/checkout"], form[action="/cart"]');
if (!isCheckoutForm) return;

const submitter = event.submitter;
const isCheckoutSubmit = !submitter || submitter.name === "checkout" || submitter.value === "checkout";
if (!isCheckoutSubmit) return;

await refreshCartState();
if (state.cartGroups.size > 1) {
event.preventDefault();
event.stopPropagation();
showBlocked(config.messages.checkoutBlocked, form);
updateCheckoutState();
}
}, true);
}

async function refreshCartState() {
try {
const response = await fetch("/cart.js", { credentials: "same-origin" });
if (!response.ok) throw new Error("Unable to load cart");

state.cart = await response.json();
state.cartGroups = new Set(
(state.cart.items || [])
.map(getGroupFromCartItem)
.filter(Boolean)
);

debug("cart groups", Array.from(state.cartGroups));
} catch (error) {
debug("cart lookup failed", error);
state.cart = null;
state.cartGroups = new Set();
}
}

function getGroupFromCartItem(item) {
return normalizeGroup(
item?.properties?._rp_collection_group ||
item?.properties?.collection_group ||
item?.properties?.Collection ||
config.productGroups[item?.handle]
);
}

function getGroupFromElement(element) {
const explicitGroup = element.dataset.rpCollectionGroup || element.dataset.collectionGroup;
if (explicitGroup) return normalizeGroup(explicitGroup);

const handle = element.dataset.productHandle || findProductHandle(element);
return normalizeGroup(config.productGroups[handle]);
}

function findProductHandle(element) {
const handleInput = element.querySelector?.('[name="handle"], [data-product-handle]');
if (handleInput?.value) return handleInput.value;
if (handleInput?.dataset?.productHandle) return handleInput.dataset.productHandle;

const productLink = element.querySelector?.('a[href*="/products/"]') || element.closest?.('a[href*="/products/"]');
if (!productLink) return null;

const match = productLink.getAttribute("href").match(/\/products\/([^/?#]+)/);
return match ? decodeURIComponent(match[1]) : null;
}

function wouldMixGroups(incomingGroup) {
return state.cartGroups.size > 0 && !state.cartGroups.has(incomingGroup);
}

function updateCheckoutState() {
const mixed = state.cartGroups.size > 1;
document.querySelectorAll(config.selectors.checkoutButtons).forEach((button) => {
button.toggleAttribute("disabled", mixed);
button.setAttribute("aria-disabled", mixed ? "true" : "false");
button.classList.toggle("rp-checkout-blocked", mixed);
});

const container = document.querySelector(config.selectors.warningContainer);
if (container) {
container.hidden = !mixed;
container.textContent = mixed ? config.messages.checkoutBlocked : "";
}
}

function showBlocked(message, target) {
if (typeof config.onBlocked === "function") {
config.onBlocked({ message, target, cart: state.cart, cartGroups: Array.from(state.cartGroups) });
return;
}

let warning = document.querySelector(config.selectors.warningContainer);
if (!warning && target) {
warning = document.createElement("div");
warning.dataset.rpMixedCollectionWarning = "";
warning.setAttribute("role", "alert");
warning.style.cssText = "margin:12px 0;padding:12px;border:1px solid #b42318;background:#fff4f2;color:#b42318;border-radius:6px;font-size:14px;line-height:1.4;";
target.insertAdjacentElement("afterend", warning);
}

if (warning) {
warning.hidden = false;
warning.textContent = message;
warning.scrollIntoView({ block: "nearest", behavior: "smooth" });
} else {
window.alert(message);
}
}

function normalizeGroup(value) {
if (!value) return null;
return String(value).trim().toLowerCase();
}

function merge(base, overrides) {
const output = { ...base, ...overrides };
output.selectors = { ...base.selectors, ...(overrides.selectors || {}) };
output.messages = { ...base.messages, ...(overrides.messages || {}) };
output.productGroups = { ...base.productGroups, ...(overrides.productGroups || {}) };
return output;
}

function debug(...args) {
if (config.debug) console.log("[RPMixedCollectionGuard]", ...args);
}
})();

© Rose Perl Technology

Did this answer your question?