Use Case: An external data source holds relevant information that needs to be served to the Checkout to inform a transactional or business process.
Approach: In this example, an external source holds customer membership information that is able to adjust the base price of one or more line items accordingly. This data informing the customer membership must be held offsite, because multiple selling channels sync to a central source of customer truth. Not all customers with status will have an active 'Online Store' account, but still expect their membership to apply price adjustments when they first visit the website.
Problem: Cart Transform Functions or Discount Functions that can modify line item prices do not have external network access outside very specific circumstances. However, Checkout UI Extensions have the ability to request Network Access, which can handle the retrieval of external data based on provided details.
I often show this simple workflow diagram to help visualise the Extensibility tools available within the Checkout, and I'm starting the realise that the arrows are under-explained. Using the strengths of each aspect independently can solve a more complex problem than any one application can solve on it's own.
The idea is that you initiate an action, which creates a unique event, which sets off another action, which creates a unique event, etc. I call it the Rube Goldberg approach to solutions.
In a truly ideal state, you're always using data that you're already storing within Shopify to use in your Functions' input queries, because it's simply faster and more reliable. But, what if the object you'd store your data in doesn't exist yet, so it's not available to query? Like a retail customer who might be known to other systems, but not to Shopify (yet)?
Here's a quick but critical adjustment to the flow to help us visualise:
Because Shopify re-executes Functions and re-renders Extensions when the Cart Object is updated, we can set up a 'cause and effect' process to benefit complex decision making in the checkout. This is where we can to get a little creative with introducing a new connection to our flow; a UI Extension can bring in external data from an available endpoint, and then apply a Cart Attribute to the Checkout to 'store' it in Shopify and update the cart object, which makes it available for a Function to use.
eg. Use a UI Extension to take a unique input from a customer, retrieve external data based on the input, update the cart object with a unique attribute as a result, which executes a Function that's looking for that unique attribute.
If we stick with our example: I want to give a 10% discount for registered customers who might be known to me through other channels, but it's their first time shopping online.
Using a custom text field entry enables us to take a membership 'number' as the very first step of the checkout, which can make it's external network call to fetch membership data from an available endpoint in our customer source of truth. Once our data's fetched, the applyAttributeChange
mutation available via the CheckoutAPI through UI Extensions attaches an attribute to the Checkout specifying the member's details have been checked.
A price modification such as a Discount Function can be configured to that applied checkout attribute into it's input query.
run.graphql
query RunInput {
cart {
attribute(key: "member_valid") {
value
}
}
}
And then we can configure the run.js to use that unique cart attribute to trigger wether or not a discount should be applied. If there is no memberValidAttribute
found, no Discount is applied.
run.js
// @ts-check
import { DiscountApplicationStrategy } from '../generated/api';
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [],
};
export function run(input) {
const { cart } = input;
// Check for the 'member_valid' attribute
const memberValidAttribute = input?.cart?.attribute?.key === 'member_valid';
if (!memberValidAttribute) {
return EMPTY_DISCOUNT;
}
return {
discounts: [
{
value: {
percentage: {
value: 10,
},
},
targets: [
{
orderSubtotal: {},
},
],
},
],
discountApplicationStrategy: DiscountApplicationStrategy.First,
};
}
Yes - the Discount function will initially execute on checkout creation prior to the Checkout UI Extension setting the attribute, but as long as it's configured to have no result until it gets the data it needs, it'll just sit idly by not doing anything until the checkout changes.
It might end up looking something like this workflow:
Considerations: A fairly major callout here is that Checkout UI Extensions run client-side, so passing sensitive data back as checkout attributes is a really bad idea. Someone who knows what they're doing could fairly easily set their own attribute, triggering any Function configured to look for that data. For example, I'd never fetch an external price for a line item via network access, and then use it as an attribute for a Cart Transform function to operate on. Theoretically, someone could set their own prices in that workflow.
Additionally, as mentioned, it'll always be more reasonable to store custom data informing a Function within Shopify proper - so once the object (such as a customer) exists, a private app should be updating that resource via the Admin API with the necessary data to mirror it with an external source.
A lot of sample extension apps are available publicly here:
Github - Shopify - Official Function Example Apps
âď¸