A shopper shops in one of two modes: delivery, or pickup at one of the store's locations. In pickup mode the platform scopes every stock figure to that location — product and variant availability, product cards, filters, search and the cart — and checkout skips shipping and shows a pickup card. A theme shows the mode, lets the shopper switch, and explains what is or is not at the chosen store. It never works out stock itself.
The no-pickup rule#
Context#
Add shopping to the ctx_needs of every section that reads it. shopping is available on every page; shop.pickup_locations comes with shop, the cart fields with cart, and variant stock with product.
Field |
What it holds |
|---|---|
|
|
|
The chosen pickup location in pickup mode; null for delivery. |
|
True when at least one location offers pickup to this shopper's market. Gate every pickup surface on it. |
|
The locations offered to the shopper, in the store's fulfillment order. Each has |
|
The cart's mode and the location it is collected from. A line the location has none of has |
|
On each entry of |
{%- assign pk_variant = nil -%}
{%- assign pk_id = product.selected_variant.id -%}
{%- if pk_id == nil and product.options.size == 0 -%}{%- assign pk_id = product.variants.first.id -%}{%- endif -%}
{%- for v in product.variants -%}{%- if v.id == pk_id -%}{%- assign pk_variant = v -%}{%- break -%}{%- endif -%}{%- endfor -%}
Switching mode: shopping_mode_set#
Post mode (delivery or pickup) and, for pickup, location_id from shop.pickup_locations. Build the form with {% form 'shopping_mode_set' %}: it posts to /shopping-mode with the CSRF token, so it works without JavaScript and returns to the page with a 303. A location outside the shopper's market is refused with STOREFRONT_PICKUP_LOCATION_UNAVAILABLE and the mode stays as it was. The choice is kept in a cookie, and a later change of country back to a market that does not offer the location resets the shopper to delivery.
{% form 'shopping_mode_set' %}
<button type="submit" name="mode" value="delivery">Delivery</button>
{% endform %}
{% form 'shopping_mode_set' %}
<input type="hidden" name="mode" value="pickup">
{%- for l in shop.pickup_locations -%}
<button type="submit" name="location_id" value="{{ l.id }}"{% if l.selected %} aria-current="true"{% endif %}>
{{ l.name }} — {{ l.address_lines | join: ', ' }} · {{ l.ready_in_label }}
</button>
{%- endfor -%}
{% endform %}
The storefront bridge submits the form in the background and its JSON response has no redirect, so the page stays put. Every stock figure on the page depends on the mode, so reload once the bridge reports success:
document.addEventListener("tf:action-success", function (event) {
if (event.detail.action === "shopping_mode_set") window.location.reload();
});
What to show#
Surface |
Delivery mode |
Pickup mode |
|---|---|---|
Header |
A Delivery chip that opens the store list. |
Pickup · store name; the same list with the store marked. |
Product page |
"Also available for pickup at N stores" from the rows whose status is not |
In stock there: "Pickup at store · ready_in_label". Not stocked there: "Not at store", the other stores with stock (one-tap |
Product cards |
Unchanged. |
An out-of-stock card is not at this store: say so instead of "Sold out". |
Cart |
Unchanged. |
"Picking up at store · ready time" with a way to change; lines with |
Decide only from what the platform reports: available_for_sale, inventory_status, the pickup_availability rows and availability_reason. Do not compute stock, count units or guess which stores carry an item.
Related: Hosted Actions for the bridge events, and CTX for the full field reference.