Add pickup availability to product pages
The pickup availability feature lets you display whether a product is available for local pickup on a product page. Customers can use this information to learn if a product available for pickup nearby without adding it to the cart.
When the customer lands on a product page, the customer IP address is used to sort the list of locations in the back-end. Fallback to alphabetical sorting is used if the location cannot be determined based on the IP address. There is also an option to sort based on proximity to the customer based on a location input field.
This tutorial is intended as an example of implementation of the pickup feature.
For more information about the implementation of pickup availability on your Shopify shop, refer to the following API references:
To implement this feature you must perform the following steps:
- Create a
store-availability.liquid
file. - Add the pickup availability container to your product page.
- Add the
StoreAvailability
function to thetheme.js
file. - Add CSS styling to the
theme.css
file.
Create the store-availability.liquid file
In the Sections directory, click
add a new section
and name itstore-availability.liquid
Paste the following code into the file:
<div class="store-availability-container"> {%- assign pick_up_availabilities = product_variant.store_availabilities | where: 'pick_up_enabled', true -%} {%- if pick_up_availabilities.size > 0 -%} <div class="store-availability-information"> {%- assign closest_location = pick_up_availabilities.first -%} {%- if closest_location.available -%} {% render 'icon-in-stock' %} {%- else -%} {% render 'icon-out-of-stock' %} {%- endif -%} <div class="store-availability-information-container"> {%- if closest_location.available -%} <p class="store-availability-information__title"> {{ 'store_availability.general.pick_up_available_at_html' | t: location_name: closest_location.location.name }} </p> <p class="store-availability-information__stock store-availability-small-text"> {{ closest_location.pick_up_time }} </p> <button class="store-availability-information__button js-modal-open-store-availability-modal store-availability-small-text" data-store-availability-modal-open aria-haspopup="dialog"> {%- if pick_up_availabilities.size == 1 -%} {{ 'store_availability.general.view_store_info' | t }} {%- else -%} {{ 'store_availability.general.check_other_stores' | t }} {%- endif -%} </button> {%- else -%} <p class="store-availability-information__title"> {{ 'store_availability.general.pick_up_unavailable_at_html' | t: location_name: closest_location.location.name }} </p> {%- if pick_up_availabilities.size > 1 -%} <button class="store-availability-information__button js-modal-open-store-availability-modal store-availability-small-text" data-store-availability-modal-open aria-haspopup="dialog"> {{ 'store_availability.general.check_other_stores' | t }} </button> {%- endif -%} {%- endif -%} </div> </div> <div class="store-availabilities-modal modal" id="StoreAvailabilityModal" role="dialog" aria-modal="true" aria-labelledby="StoreAvailabilitiesModalProductTitle" > <div class="store-availabilities-modal__header"> <div class="store-availabilities-modal__product-information"> <h2 id="StoreAvailabilitiesModalProductTitle" class="store-availabilities-modal__product-title" data-store-availability-modal-product-title > </h2> <p class="store-availabilities-modal__variant-title store-availability-small-text" data-store-availability-modal-variant-title > {{ product_variant.title }} </p> </div> <button type="button" class="store-availabilities-modal__close js-modal-close-store-availability-modal text-link" aria-label="{{ 'general.accessibility.close_modal' | t }}" > {% render 'icon-close' %} </button> </div> <ul class="store-availabilities-list" role="list"> {%- for availability in pick_up_availabilities -%} <li class="store-availability-list__item"> <h3 class="store-availability-list__location"> {{availability.location.name}} </h3> <div class="store-availability-list__stock store-availability-small-text"> {%- if availability.available -%} {% render 'icon-in-stock' %} {{ 'store_availability.general.pick_up_available' | t }}, {{ availability.pick_up_time | downcase }} {%- else -%} {% render 'icon-out-of-stock' %} {{ 'store_availability.general.pick_up_currently_unavailable' | t }} {%- endif -%} </div> {%- assign address = availability.location.address -%} <address class="store-availability-list__address"> {{ address | format_address }} </address> {%- if address.phone.size > 0 -%} <p class="store-availability-list__phone store-availability-small-text"> {{ address.phone }}<br> </p> {%- endif -%} </li> {%- endfor -%} </ul> </div> {%- endif -%} </div> {% schema %} { "name": {}, "settings": [] } {% endschema %}
Click Save.
Add the pickup availability container to the product page
In the Sections directory, go to the
product.liquid
file.Find where you would like the pickup availability options to appear and paste the following code:
<div class="product-single__store-availability-container" data-store-availability-container data-product-title="{{ product.title | escape }}" data-has-only-default-variant="{{ product.has_only_default_variant }}" data-base-url="{{ shop.url }}{{ routes.root_url }}" > </div>
Click Save.
Add the StoreAvailability function
In the Assets directory, go to the
Theme.js
file.At the bottom of the file, paste the following code:
theme.StoreAvailability = (function() { var selectors = { storeAvailabilityModalOpen: '[data-store-availability-modal-open]', storeAvailabilityModalProductTitle: '[data-store-availability-modal-product-title]', storeAvailabilityModalVariantTitle: '[data-store-availability-modal-variant-title]' }; var classes = { hidden: 'hide' }; function StoreAvailability(container) { this.container = container; this.productTitle = this.container.dataset.productTitle; this.hasOnlyDefaultVariant = this.container.dataset.hasOnlyDefaultVariant === 'true'; } StoreAvailability.prototype = Object.assign({}, StoreAvailability.prototype, { updateContent: function(variantId) { var variantSectionUrl = this.container.dataset.baseUrl + '/variants/' + variantId + '/?section_id=store-availability'; var self = this; var storeAvailabilityModalOpen = self.container.querySelector( selectors.storeAvailabilityModalOpen ); this.container.style.opacity = 0.5; if (storeAvailabilityModalOpen) { storeAvailabilityModalOpen.disabled = true; storeAvailabilityModalOpen.setAttribute('aria-busy', true); } fetch(variantSectionUrl) .then(function(response) { return response.text(); }) .then(function(storeAvailabilityHTML) { if (storeAvailabilityHTML.trim() === '') { return; } self.container.innerHTML = storeAvailabilityHTML; self.container.innerHTML = self.container.firstElementChild.innerHTML; self.container.style.opacity = 1; // Need to query this again because we updated the DOM storeAvailabilityModalOpen = self.container.querySelector( selectors.storeAvailabilityModalOpen ); if (!storeAvailabilityModalOpen) { return; } storeAvailabilityModalOpen.addEventListener( 'click', self._onClickModalOpen.bind(self) ); self.modal = self._initModal(); self._updateProductTitle(); if (self.hasOnlyDefaultVariant) { self._hideVariantTitle(); } }); }, clearContent: function() { this.container.innerHTML = ''; }, _onClickModalOpen: function() { this.container.dispatchEvent( new CustomEvent('storeAvailabilityModalOpened', { bubbles: true, cancelable: true }) ); }, _initModal: function() { return new window.Modals( 'StoreAvailabilityModal', 'store-availability-modal', { close: '.js-modal-close-store-availability-modal', closeModalOnClick: true, openClass: 'store-availabilities-modal--active' } ); }, _updateProductTitle: function() { var storeAvailabilityModalProductTitle = this.container.querySelector( selectors.storeAvailabilityModalProductTitle ); storeAvailabilityModalProductTitle.textContent = this.productTitle; }, _hideVariantTitle: function() { var storeAvailabilityModalVariantTitle = this.container.querySelector( selectors.storeAvailabilityModalVariantTitle ); storeAvailabilityModalVariantTitle.classList.add(classes.hidden); } }); return StoreAvailability; })();
Click Save.
Add CSS for the product page container
In the Assets directory, go to the
Theme.css
file.At the bottom of the file, paste the following code:
.store-availability-small-text { font-size: calc(((var(--font-size-base) - 2) / (var(--font-size-base))) * 1em); } .store-availability-information { display: flex; margin-left: 5px; } .store-availability-information-container { margin-left: 5px; } .store-availability-information__title { margin-bottom: 0; } .store-availability-information__stock { margin-bottom: 0; } .store-availability-information__button { text-decoration: underline; cursor: pointer; border: none; padding: 0; background: transparent; margin-top: 8px; } .store-availability-container .icon { width: 12px; height: 12px; margin: 4px 4px 0 0; } .store-availability-container .icon-in-stock { fill: #00730B; } .store-availability-container .icon-out-of-stock { fill: #DD2200; } .store-availability-container .icon-close { width: 18px; height: 18px; margin: 0; } .store-availability-container .store-availability-list__stock .icon { margin: 0 2px 3px 0; } .store-availability-container .store-availability-list__confirm-address .icon-unverified-address { margin: 0 0 3px 0; } .store-availability-container .store-availability-list__confirm-address .icon-unverified-address .icon-unverified-address__exclamation { fill: var(--color-body-text); } .store-availability-container .store-availability-list__confirm-address .icon-unverified-address .icon-unverified-address__circle { stroke: var(--color-body-text); fill: transparent; } .store-availabilities-modal { z-index: 3; width: 375px; left: auto; border: 1px solid var(--color-border); box-sizing: border-box; box-shadow: -4px 4px 6px rgba(58, 58, 58, 0.04); } .store-availabilities-modal--active { display: flex; flex-direction: column; transform: translateY(0); opacity: 1; } @media only screen and (max-width: 749px) { .store-availabilities-modal--active { width: 100%; height: 100%; overflow: hidden; z-index: 9999; } } .store-availabilities-modal__header { display: flex; justify-content: space-between; margin: 16px 0 16px 0; padding: 0 24px 0 24px; } .store-availabilities-modal__product-title { font-size: calc(((var(--font-size-base) + 2) / (var(--font-size-base))) * 1em); text-transform: none; margin-bottom: 0; letter-spacing: 0; } .store-availabilities-list { overflow-y: auto; padding: 0 20px 0 20px; } .store-availabilities-modal__close { margin: -14px -14px 0 0; padding: 14px; align-self: start; } .store-availability-list__item { padding-bottom: 16px; } .store-availability-list__item::before { content: ""; display: block; margin: 0 0 16px 0; border-bottom: 1px solid var(--color-border); } .store-availability-list__address { font-size: calc(((var(--font-size-base) - 2) / (var(--font-size-base))) * 1em); font-style: normal; margin-bottom: 0; } .store-availability-list__location { font-size: var(--font-size-base); margin-bottom: 8px; } .store-availability-list__stock { margin-top: 8px; margin-bottom: 16px; } .store-availability-list__phone { margin-bottom: 0; }
- Click Save.