Software Development Kit (SDK)

Introduction

Retailers with custom built ecommerce sites can use the HubBox SDK to implement the pickup solution at checkout. The SDK can also be used by retailers on larger ecommerce platforms as well as headless ecommerce sites. The SDK provides a highly flexible integration method and can therefore be used to create a more custom pickup experience.

Ways to implement

"Launch Experience" way

The "launch" experience is the UI in the checkout which presents the customer with the option to choose pickup. The implementation of the launch experience is very important as this will drive customer engagement and therefore the return on investment from offering a pickup service. It is therefore critical that the launch experience is:

"Shipping method" way

The shipping method launch experience is where the user is presented with the choice of pickup alongside the usual shipping options such as standard, express, and next day delivery services.

Presenting the pickup option as a shipping method also enables retailers to clearly display the price of this option, especially effective if the pickup delivery is offered at a cheaper rate than home delivery options.

"Shipping address" way

The shipping address launch experience is where the user is presented with a choice between Pickup or Home Delivery before they enter their shipping address. This can often be displayed as a set of buttons or toggles (often side-by-side) above the shipping address form.

Implementing the widget

Installation

The HubBox widget is distributed via a JavaScript package and can be accessed via Node Package Manager (NPM). More information on this process can be found in the Manager.js reference documentation. You will need to include two dependencies within the package.json:

  1. @hubbox/single-widget-manager
  2. @hubbox/web-components

If you do not have a JavaScript build process that uses JavaScript dependencies, please contact the HubBox Integrations team and they will be able to provide a CDN URL or a fixed minified version of single-widget-manager.

Design

Embedded

To boot the Widget, you will first need to add a target div to a page where you wish the Widget to be displayed. Below is an example for a project using ES6 JavaScript:

<div id="hubbox-widget" class="hubbox-hidden"></div>

Then, add an input field that would be used to enter the address:

<label for="hubbox-search-input">Postcode</label> <input type="text" id="hubbox-search-input" name="hubbox-search-input" placeholder="EC1V 3RR" />

Finally, add the button that would be used to trigger the search:

<button id="hubbox-search-button" disabled>Search</button>
.hubbox-hidden {
  display: none;
}

Then, to initialize the Widget do the following:

const singleWidgetManager = new SingleWidgetManager({
  container: document.getElementById("hubbox-widget"),
  iframeUrl: SingleWidgetManager.iframeUrls.PRODUCTION, // or SingleWidgetManager.iframeUrls.SANDBOX
  healthCheck: false,
  isDebug: false,
  iframeParams: {
    configId: "global", // you will have a dedicated config.
    locale: "en" // tweak based on your store locale.
  }
});

If using TypeScript add the following line to the top of the file: import "@hubbox/web-components";

Then, when you click on the "search" button, emit the search query:

const searchButtonElement = document.getElementById("hubbox-search-button");
searchButtonElement.addEventListener("click", function () {
  const searchQuery = document.getElementById("hubbox-search-input").value;
  singleWidgetManager.events.emit(singleWidgetManager.topics.emit.MAP_SEARCH_QUERY, searchQuery);
});

...where the searchQuery is a postcode, city or any other part of the address.

.Also, in the same callback, add the logic to display the widget container:

document.getElementById("hubbox-widget").classList.remove("hubbox-hidden");

The same applies to hiding it again - needs to be performed manually (inside the "COLLECT_POINT_CONFIRMED" callback (more on that later)):

document.getElementById("hubbox-widget").classList.add("hubbox-hidden");

Modal

Like in the previous step, instantiate the SingleWidgetManager class.

const singleWidgetManager = new SingleWidgetManager({
  deferRender: true,
  iframeUrl: SingleWidgetManager.iframeUrls.PRODUCTION, // or SingleWidgetManager.iframeUrls.SANDBOX
  healthCheck: false,
  isDebug: false,
  iframeParams: {
    configId: "global", // you will have a dedicated config.
    locale: "en" // tweak based on your store locale.
  }
});

Please note that we don't specify a container element here, instead we set deferRender to true. Also, we don't necessarily need to have an input field to store the search query, as once opened, the widget has a built-in input field. The difference is that when the external input field is not present, the default location will be used ("London" for UK, and "New York" for US; this is actually configurable per config).

Next, we need to create the HbWidgetModal instance. You can add it either to the DOM like this:

<hb-widget-modal></hb-widget-modal>

...or to create it dynamically in JavaScript like this (can be done either straight after the page has loaded or when the search is performed (on the fly):

const hbWidgetModal = document.createElement("hb-widget-modal");

Then, configure it:

const hbWidgetModal = document.querySelector("hb-widget-modal");
hbWidgetModal.locale = "en";
hbWidgetModal.open = false;

const singleWidgetManager = getSingleWidgetManager();
hbWidgetModal.appendChild(singleWidgetManager.iframeElem);

If using TypeScript add the following line to the top of the file: import "@hubbox/web-components";

In contract to the previously mentioned optional elements, some sort of CTA to launch the widget is still required; could be a button or a link, doesn't really matter. However, when it is clicked you would need to open the modal:

const hbWidgetModal = document.querySelector("hb-widget-modal");
hbWidgetModal.open = true;

Then, inside the "COLLECT_POINT_CONFIRMED" callback you would need to close the modal:

const hbWidgetModal = document.querySelector("hb-widget-modal");
hbWidgetModal.open = false;

Registering callbacks

To integrate actions within the Widget to your site, the manager.js package contains a PubSub interface where you can register various callbacks/subscriptions.

In the below example, the confirmedCallback is subscribed to the COLLECT_POINT_CONFIRMED event. This event is triggered when a user confirms a location within the Widget. This will then emit this event to all subscriptions. To learn more about different subscribable events and the event schemas, please reference the Manager.js reference documentation.

onCollectPointConfirmed = function (collectPoint) {
  // Implement your logic (update shipping methods, populate address fields, etc.) here.
};
singleWidgetManager.events.subscribe(singleWidgetManager.topics.subscribe.COLLECT_POINT_CONFIRMED, onCollectPointConfirmed);

For a minimum viable implementation, in order for the HubBox Widget to function, the following subscriptions will need to be implemented with some or all of the following business processes:

COLLECT_POINT_CONFIRMED

  1. Update Shipping Address with chosen pickup location address
  2. Add location ID and other metadata to checkout/session/cart object
  3. Render confirmation UI
  4. Recalculate shipping rates (if applicable)
  5. Close modal
  6. Move user to next step in the checkout
  7. Update checkout summary

COLLECT_POINT_UNSELECTED

  1. Do inverse operation of COLLECT_POINT_CONFIRMED
  2. Reset checkout to “Home Delivery”
  3. Recalculate shipping rates (if applicable)
  4. Clear pickup location address data

Available callbacks

This is the list of all available callbacks that you can subscribe to:

WIDGET_READY = "WIDGET_READY",
SEARCH_REQUEST = "SEARCH_REQUEST",
SEARCH_FAILED = "SEARCH_FAILED",
COLLECT_POINT_SELECTED = "COLLECT_POINT_SELECTED",
COLLECT_POINT_UNSELECTED = "COLLECT_POINT_UNSELECTED",
COLLECT_POINT_CONFIRMED = "COLLECT_POINT_CONFIRMED",
BOOT_FAILED = "BOOT_FAILED",

Populating address fields

The previously discussed callback that is executed whenever the COLLECT_POINT_CONFIRMED event is emitted, should have an argument - messageAndTopic. It's message property holds a data object for the pickup point that was confirmed:

const onCollectPointConfirmed = (messageAndTopic) => {
  const collectPointData = messageAndTopic.message;
  const address = collectPointData.address;
};

The address contains the following properties:

street1: string | null;
street2?: string | null;
street3?: string | null;
street4?: string | null;
city?: string | null;
postcode?: string | null;
region?: string | null;
latitude?: number | null;
longitude?: number | null;

Implementing "Pickup Confirmation" component

The "pickup confirmation" component is the UI in the checkout displayed after a customer has selected a pickup location from the HubBox Widget. This UI confirms to the customer that they have chosen to pick up their order and typically displays the address and opening times of their chosen pickup location.

HubBox provides a confirmation experience component that can be implemented and customized in your checkout. Alternatively, you can build your own UI to confirm that the customer has chosen pickup.

Add the component to the DOM (and hide it):

<hb-core-pickup-confirmation class="hubbox-hidden" locale="en"></hb-core-pickup-confirmation>

When the pickup point has been successfully confirmed (COLLECT_POINT_CONFIRMED event), display the component by removing the hubbox-hidden class.

Passing pickup location data to the ecommerce backend

Once a customer has chosen a pickup location, details of their chosen location (e.g. the chosen location's address and location ID) will need to be passed from the frontend to the object on the backend used to hold the checkout data. This ensures that when the customer completes the checkout process, their order includes all of the required data. Below are three options for storing the pickup location data:

Hidden fields

Passing data via hidden input fields within the checkout is the most common integration method. The main advantage is that it’s easy to implement and debug. Hidden fields are not appropriate in a single-page application (SPA) checkout or in headless frontends where additional REST APIs should be considered instead. Below is an example where hidden fields are used to store pickup location data.

<div class="osc-short input-box input-city">
<label for="shipping:city">City <span class="required">*</span></label>
<input autocomplete="false" type="text" name="shipping[city]" value="" title="City" class="required-entry input-text" id="shipping:city" placeholder="City *">
</div></li><li class="clearfix enter-manually li-after-city enter-manually--hidden">
<div class="osc-short input-box input-postcode">
<label for="shipping:postcode">Zip/Postal Code <span class="required">*</span></label>
<input autocomplete="false" type="text" title="Zip/Postal Code" name="shipping[postcode]" id="shipping:postcode" value="" class="validate-zip-international required-entry input-text" placeholder="Zip/Postal Code *">
</div>
<input type="hidden" name="shipping[locationId]" id="hubbox-cpid">

New REST API

When using single-page application checkouts, order information is often communicated to the backend systems via REST APIs rather than form submission. In this case, extending the REST API for shipping address or adding new rest routes for pickup and deselecting pickup is the recommended approach. Below is an example of a JavaScript controller which implements this process:

"use strict";
/**
 * Extension to default CheckoutShippingServices controller to provide custom
 * implementation for Address validation on shipping page for single shipment.
 * @module  controllers/CheckoutShippingServices
 */

var server = require("server");
server.extend(module.superModule);
var csrfProtection = require("*/cartridge/scripts/middleware/csrf");
var HubBoxHelper = require("*/cartridge/scripts/helpers/HubBoxHelper");
var ShippingHelper = require("*/cartridge/scripts/checkout/shippingHelpers");

server.post("HubBox", server.middleware.https, csrfProtection.validateAjaxRequest, function (req, res, next) {
  // Load up DB, Statement Management, Other helpsers
  var BasketMgr = require("dw/order/BasketMgr");
  var Locale = require("dw/util/Locale");
  var OrderModel = require("*/cartridge/models/order");
  // Load current basket/session/cart/checkout for end user
  var currentBasket = BasketMgr.getCurrentBasket();
  var shipment = currentBasket.defaultShipment;
  // Validate post data
  if (HubBoxHelper.collectPointIdIsValid(req.form.collectPointId)) {
    // Copy Collect Point info and meta data over to Shipment
    // Shipment in this case contain shipping address and method
    HubBoxHelper.copyHubBoxDataToShipment(
      shipment,
      req.form.collectPointId,
      req.form.collectPointName,
      req.form.collectPointType,
      req.form.collectPointNetwork
    );
    // append json to response to integrate into front end
    res.json({ state: "Order is Click and collect" });
  } else {
    HubBoxHelper.clearHubBoxDataFromShipment(shipment);
    res.json({ state: "Click and collect removed" });
  }
  var currentLocale = Locale.getLocale(req.locale.id);
  var basketModel = new OrderModel(currentBasket, {
    usingMultiShipping: false,
    shippable: true,
    countryCode: currentLocale.country,
    containerView: "basket"
  });
  // Append more information into response object
  res.json({
    order: basketModel,
    shippingForm: server.forms.getForm("shipping")
  });
  return next();
});

module.exports = server.exports();

Cookies

In the same way that hidden fields can be used to transfer the location ID for form submission, a cookie can also be used when checkout is driven by form submission. The only possible case to implement this is when using a platform such as Aptos where changes to forms template within the checkout are impossible and modifying the form with JavaScript is also impossible. However, the controller layer has access to the HTTP cookies through something like: $_COOKIES / HttpServletRequest / Request.x.

In the same way that hidden fields can be used to transfer the location ID for form submission, a cookie can also be used when checkout is driven by form submission. The only possible case to implement this is when using a platform such as Aptos where changes to forms template within the checkout are impossible and modifying the form with JavaScript is also impossible. However, the controller layer has access to the HTTP cookies through something like: $\_COOKIES / HttpServletRequest / Request.x.

Customization & styling

If using the HubBox web components, these can be styled via CSS using the parts API. More information and CSS examples can be found in the HubBox Components documentation.

The HubBox Widget is the UI which presents customers with available pickup locations via a map and pin interface. The Widget sits inside a responsive iframe which can either be embedded on the checkout page or displayed as a modal. More information and configuration examples can be found in the Widget Configuration documentation.

Advanced features

Hiding pickup option for ineligible baskets

Retailers with diverse product catalogs may be required to exclude certain products from the pickup service. In this instance, you will need to hide the pickup option for baskets that contain ineligible items. For example, if the customer has an item in their basket that is too large for a pickup location, you will need to ensure that they cannot see the pickup option at checkout.

Initial considerations

JavaScript example
function isBasketEligibleForPickup(cartItems) {
  return cartItems.every((item) => item.isEligibleForPickup); // Assume 'isEligibleForPickup' exists on items
}

// ... In your checkout area ...
const cartItems = getCartItems(); // Replace with your platform's method
const pickupOptionElement = document.getElementById("pickupOption");

if (isBasketEligibleForPickup(cartItems)) {
  pickupOptionElement.style.display = "block"; // Show pickup option
} else {
  pickupOptionElement.style.display = "none"; // Hide pickup option
}

Shipping method filtering

If you use multiple carriers or wish to offer pickup at a different price/service level, you will need to show and hide particular shipping rates depending on whether the customer has chosen home delivery or pickup.

To achieve shipping rate management, there are two approaches you can take. First, you can simply hide rates that are not suitable for pickup when a customer selects a pickup location. This way, only relevant rates will be shown to the customer. Alternatively, you may choose to create specific pickup shipping rates, especially if you want to offer a discounted shipping rate for pickup compared to home delivery. In this case, you would display these pickup rates when customers choose the pickup option and hide them when a customer selects home delivery. By implementing either of these methods, you can ensure that customers are presented with accurate and appropriate shipping rates based on their chosen pickup or delivery preference.

Initial considerations
JavaScript example
function filterShippingRates(availableRates, isPickupOrder) {
  if (isPickupOrder) {
    return availableRates.filter((rate) => rate.id === "local_pickup"); // Only keep "Local Pickup"
  } else {
    return availableRates.filter((rate) => rate.id !== "local_pickup"); // Filter out "Local Pickup"
  }
}

// ... Somewhere in your checkout process ...
const isPickupOrder = order.hasFlag("pickup"); // Replace with your logic
const allShippingRates = getAvailableShippingRates(); // Replace with your platform's method
const filteredRates = filterShippingRates(allShippingRates, isPickupOrder);

// Update the UI or recalculate totals using 'filteredRates'

Other features

HubBox can also offer additional features upon request:

These features require HubBox customization so please contact the HubBox Integrations team if you would like to find out more at clientsupport@hub-box.com.

Example integration

To access an example SDK integration project, please ensure you are logged in so you can access the download link below. If you do not have a Console login, please contact your HubBox representative.

Download SDK integration example