> ## Documentation Index
> Fetch the complete documentation index at: https://developers.appstle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript events and API for Appstle Bundles on Shopify themes

> The window._ABConfig members, events, cart refresh helpers and slider cart API that Appstle Bundles exposes to JavaScript on a Shopify theme.

On a Shopify theme, the Appstle Bundles app embed and the Appstle Slider Cart app embed expose a small set of JavaScript globals, methods and events. This page lists each one, when it fires and what it carries. Use them in your theme or in a script you add to the storefront, for example to read bundle rules, to re-read the cart after Bundles changes it, or to react when the slider cart opens.

These interfaces exist only on a Shopify theme where the app embed is on. A headless storefront doesn't run Appstle's scripts: read the rules from the shop metafield and add bundles to the cart with Shopify's APIs, as [Build your own frontend](https://developers.appstle.com/bundles/headless-overview.md) explains.

> [!WARNING]
>
> **Build on these globals, methods and events, not on the widgets' markup.** The HTML structure, CSS class names and element IDs of the Bundles widgets aren't part of this interface. They can change whenever a widget's design is updated, so code or CSS that selects widget elements can stop working after an update. For changes to these globals, methods and events, see the [API changelog](https://developers.appstle.com/bundles/api-changelog.md). If you need full control of the markup, [build your own frontend](https://developers.appstle.com/bundles/headless-overview.md).

## Before you start

* Turn on the **Appstle Bundles** app embed in the theme editor (**App embeds**). The slider cart interfaces also need the **Appstle Slider Cart** app embed.
* The embed adds its script at the end of the page body, so `window._ABConfig` doesn't exist yet while scripts earlier in the page run. Run your code from a script with `defer`, or in a `DOMContentLoaded` listener.
* The cart refresh helpers are in place by `DOMContentLoaded`.
* On paths the merchant lists under **Bundle Excluded Paths** on the **Shop Settings** page, the embed loads nothing and `window._ABConfig` is undefined. Check for it before you call a method, for example with `window._ABConfig?.whenConfigReady(...)`.

## Read bundle configuration

The Bundles app embed sets `window._ABConfig`. These are its documented members:

* `bundle_rules` (array): the store's active bundle rules that the page has, in the same shape as the `appstle_bundles.bundle_rules` shop metafield. Several fields are JSON-encoded strings; see [Reading bundle configuration](https://developers.appstle.com/bundles/headless-overview.md#reading-bundle-configuration).
* `bundle_setting` (object): the store's Bundles settings, including the text labels the widgets show.
* `whenConfigReady(callback)`: runs `callback` once the settings and the page's rules are in place, or at once on a page that loaded with them.
* `loadAllBundleRules()`: returns a promise that resolves with every rule. It never rejects.
* `hasBundleRulesFor(productIds)`: returns `true` when the page has the rules for every product in `productIds`, an array of numeric Shopify product IDs (numbers or strings, not GIDs), or when the page already has all rules.
* `getProductDiscountedPricing(options)`: returns a preview of the bundle discount for one variant. See [Preview a discount](#preview-a-discount).

### Rules and settings can arrive after the page loads

On some stores, each page starts with `window._ABConfig.bundle_rules` empty and only the labels in `window._ABConfig.bundle_setting`, and the embed fetches the rest right away. On other stores, the page leaves out the volume discount and Buy X Get Y rules that apply to specific products, unless the page is the product page of one of those products or one of them is in the cart. There, the embed fetches the rest only when code calls `loadAllBundleRules()`, so call it rather than wait for an event.

Both happen only while the app embed's **Load bundle rules only where needed** setting is on, which it is by default. With it off, every page loads with every rule.

* `whenConfigReady(callback)` runs `callback` once the settings and the page's rules are in place: when the fetch finishes on the first kind of store, and at once on the second. If that fetch fails, it still runs `callback`, with `bundle_rules` empty and only the labels in `bundle_setting`.
* `loadAllBundleRules()` fetches the rules the page doesn't have and resolves with every rule. If they can't load, it resolves with the rules the page has, which can be an empty list, and the next call tries again.
* When the rest arrive, the embed fills `bundle_rules` (and, on the first kind of store, `bundle_setting`) in place and dispatches `appstle-bundles:rules-loaded` on `document`. Don't replace the array or the object, and read them again after the event.

```javascript theme={null}
window._ABConfig.whenConfigReady(() => {
  const rules = window._ABConfig.bundle_rules;
  const settings = window._ABConfig.bundle_setting;
  // Render your bundle UI from `rules` and `settings`.
});
```

To read the rules for products other than the one on the page, wait for all of them:

```javascript theme={null}
const rules = await window._ABConfig.loadAllBundleRules();
const volumeRules = rules.filter(
  (rule) => rule.bundleType === 'VOLUME_DISCOUNT'
);
```

On a page that loaded with every rule, `whenConfigReady` calls its callback at once and `loadAllBundleRules()` resolves at once with the same array as `bundle_rules`. On such a page, `appstle-bundles:rules-loaded` never fires.

### Preview a discount

`window._ABConfig.getProductDiscountedPricing(options)` returns the best volume discount or **Product Discount** a variant gets at a quantity. It runs in the browser and makes no request.

| Option | Type | Description |
| - | - | - |
| `variantId` | number or string | Required. The numeric Shopify variant ID. |
| `amount` | number | Required. The unit price in the store's currency, in major units (`18` for 18.00), not cents. |
| `quantity` | number | Required. The number of units. |
| `sellingPlanId` | number or `null` | The selling plan for a subscription purchase, or `null` for a one-time purchase. If you leave it out, rules for both purchase types count. |

```javascript theme={null}
const preview = window._ABConfig.getProductDiscountedPricing({
  variantId: 44123456789012,
  amount: 18,
  quantity: 2,
  sellingPlanId: null,
});
```

```json theme={null}
{
  "variantId": 44123456789012,
  "quantity": 2,
  "amount": 18,
  "totalAmount": 36,
  "discountType": "PERCENTAGE",
  "discountValue": 10,
  "discountAmount": 3.6,
  "discountedPrice": 32.4,
  "discountConfigure": "BOTH"
}
```

| Field | Type | Description |
| - | - | - |
| `variantId`, `quantity`, `amount` | number | The values you passed. |
| `totalAmount` | number | `amount` × `quantity`. |
| `discountType` | string | `PERCENTAGE` or `FIXED_AMOUNT`. |
| `discountValue` | number | The percentage, or the amount off `totalAmount`. |
| `discountAmount` | number | The discount on `totalAmount`. |
| `discountedPrice` | number | `totalAmount` minus `discountAmount`. |
| `discountConfigure` | string | The purchases the rule applies to: `ONE_TIME`, `SUBSCRIPTION` or `BOTH`. |

When the tier that applies has no discount, such as a 1 item tier at the standard price, `discountValue` and `discountAmount` are `0` and `discountedPrice` equals `totalAmount`. When no rule or tier applies, for example at a quantity below a rule's lowest tier or for a variant no rule includes, every discount field is `undefined`.

* Only active volume discount and Product Discount rules that include the variant count. Buy X Get Y rules don't.
* Rules that target collections count only on a product page, where the helper matches them against that page's product.
* Rules limited to customer tags or to a number of uses per customer count only for a signed-in shopper they allow.

It reads `window._ABConfig.bundle_rules`. On stores where rules arrive after the page loads, call it inside `whenConfigReady`, and for a product other than the one on the page, after `await loadAllBundleRules()`. Called earlier, it returns no discount and starts loading the missing rules.

It returns an empty array, and logs an error to the console, when `variantId` is missing or the merchant has turned on **Disable App Functionality**. The Shopify Function applies the discount at checkout, so treat the result as a preview. See [Previewing the discount](https://developers.appstle.com/bundles/headless-overview.md#previewing-the-discount).

## Events

Bundles dispatches these events on a Shopify theme. Each one names the target to listen on.

* `appstle-bundles:rules-loaded` is dispatched on `document`, with no `event.detail`. It fires when the embed has filled `window._ABConfig.bundle_rules` with rules it fetched after the page loaded.
* `appstle-bundles:reconcile` is dispatched on `window`, with `event.detail` set to `{ state, id }`. It fires when Bundles checks the cart after a change, to keep bundle quantities and discounts in step. See [Cart reconcile](#cart-reconcile).
* `appstleBundlesAppliedVolumeDiscount` is dispatched on `document` by the volume discount widget on a product page. It fires when the widget renders and each time the selected tier, its quantity or the variant changes, with `event.detail` set to the selected tier. `event.detail` is `null` when no tier is selected or the widget goes away. Only widgets whose **Widget theme version** is **Modern**, the default, send it; **Legacy** widgets don't. See [Applied volume discount](#applied-volume-discount).
* To get the current tier at any time, dispatch `appstleBundlesAppliedVolumeDiscount:request` on `document`. The widget answers with `appstleBundlesAppliedVolumeDiscount`.
* `appstle-bundle-product-added-to-cart` is dispatched on both `document` and `window`, with `event.detail` set to the bundle's `uniqueRef` (a string). It fires when a fixed pricing bundle widget's own button has added the bundle to the cart. Right after it, the widget follows the bundle's redirect setting: it goes to the cart, checkout or a custom URL, or opens or refreshes the cart drawer. The page can unload as soon as your listener returns, so keep the listener synchronous, and send analytics with `navigator.sendBeacon`. It doesn't fire when the theme's add-to-cart button adds a bundle from a product page.
* `AppstleBundles:BuildABox:OpenAppstleCartDrawer` is dispatched on `window`, with no `event.detail`. It fires when a shopper clicks a link to `/cart` on a page that runs the Bundles widget script, while the Appstle Slider Cart is off. See [Cart links on bundle widget pages](#cart-links-on-bundle-widget-pages).

### Applied volume discount

For example, the volume discount widget on a product page sends this `event.detail` after the shopper chooses the 3 items tier (15 percent):

```json theme={null}
{
  "productId": 15100000000001,
  "variantId": 44123456789012,
  "uniqueRef": "a1b2c3d4e5",
  "isSubscriptionFeatureEnabled": false,
  "type": "VOLUME_DISCOUNT_WIDGET",
  "purchaseType": "BOTH",
  "quantity": 3,
  "discountBasedOn": "QUANTITY",
  "discountType": "PERCENTAGE",
  "discount": 15,
  "value": 3
}
```

| Field | Type | Description |
| - | - | - |
| `productId`, `variantId` | number | The product and variant the widget shows. |
| `uniqueRef` | string | The `uniqueRef` of the bundle rule. |
| `type` | string | Always `VOLUME_DISCOUNT_WIDGET`. |
| `purchaseType` | string | The purchases the rule applies to: `ONE_TIME`, `SUBSCRIPTION` or `BOTH`. |
| `quantity` | number | The quantity the shopper is buying on the selected tier: the tier's quantity, or the higher quantity the shopper picked with the tier's quantity selector. `1` for a tier based on spend. |
| `discountBasedOn` | string | `QUANTITY` or `AMOUNT`. |
| `discountType` | string | `PERCENTAGE` or `FIXED_AMOUNT`. |
| `discount` | number | The tier's discount: a percentage or an amount. |
| `value` | number | The tier's configured quantity, or its spend threshold for a tier based on spend. It stays the same when the shopper picks a higher quantity. |

For a Buy X Get Y tier, `quantity` and `value` are X, the quantity to buy, `discountType` is `PERCENTAGE`, and `discount` is the free share of the X plus Y units: Y ÷ (X + Y) × 100. Fields that aren't in the table, such as `isSubscriptionFeatureEnabled`, aren't documented.

```javascript theme={null}
document.addEventListener(
  'appstleBundlesAppliedVolumeDiscount',
  (event) => {
    const tier = event.detail;
    if (!tier) return; // no tier selected
    showMyPriceNote(tier.quantity, tier.discount);
  }
);
document.dispatchEvent(
  new CustomEvent('appstleBundlesAppliedVolumeDiscount:request')
);
```

### Cart reconcile

On stores with an active volume discount, combo or fixed pricing bundle, Bundles checks the cart after each cart change and can change it again so bundle quantities and discounts stay in step. It dispatches `appstle-bundles:reconcile` on `window` around that check, so a cart drawer can wait for it before it re-reads the cart.

* `event.detail.state` is `scheduled` when a check is coming, and `done` when it has finished, whether or not it changed the cart.
* `event.detail.id` is a string that identifies the check. Cart changes made in quick succession share one `id`.

Bundles sees cart changes made with `fetch`: a `POST` to `/cart/add.js`, `/cart/change.js` or `/cart/update.js`, with the URL as a string. Changes made with `XMLHttpRequest` don't start a check.

Re-read the cart on `done`, which usually arrives a few seconds after `scheduled`. If it hasn't arrived 15 seconds after `scheduled`, re-read the cart anyway.

```javascript theme={null}
window.addEventListener('appstle-bundles:reconcile', (event) => {
  if (event.detail.state === 'done') {
    refreshMyCartDrawer();
  }
});
```

### Cart links on bundle widget pages

On a page that runs the Bundles widget script, such as a page with a fixed pricing, dynamic pricing or sectioned bundle widget, the script cancels the click on every link whose `href` is exactly `/cart` and that's on the page when the script loads. It dispatches `AppstleBundles:BuildABox:OpenAppstleCartDrawer` on `window` instead.

The shopper goes to the cart page only when the **Enable Redirect to Cart Page On Cart Icon Click** setting on the **Bundle Configurations** page is on (`bundle_setting.redirectToCartPageOnCartIconClick`). With the setting off, the click does nothing until a listener opens a cart. To open your theme's cart drawer, listen for the event:

```javascript theme={null}
window.addEventListener(
  'AppstleBundles:BuildABox:OpenAppstleCartDrawer',
  () => openMyCartDrawer()
);
```

When the Appstle Slider Cart is on, it handles clicks on cart links itself: it opens its drawer, the shopper stays on the page, and you don't need this listener.

## Cart refresh helpers

Bundles changes the cart in the background, so your theme's cart doesn't know about the change. After it does, it calls these helpers to make the theme re-read the cart. They're best-effort: they guess which cart and drawer your theme uses. If you control your theme's cart, refresh it yourself when you hear the events below. You can also call the helpers after your own code changes the cart in the background.

These methods are on `window._ABConfig` from `DOMContentLoaded`.

* `triggerCartRefresh()`: returns a promise. It dispatches the cart events below, re-renders the theme's cart drawer and cart count from Shopify, and on the cart page re-renders the cart sections. On the cart page, it reloads the page when it can't find the cart sections.
* `openCartDrawer()`: opens the theme's cart drawer. It does nothing on the cart page. On a theme without a cart drawer, it can click the header's cart link, which takes the shopper to the cart page.
* `refreshCartDrawer()`: returns a promise. It calls `triggerCartRefresh()`, then, if the drawer is open, opens it again so it shows the new cart.

When the Appstle Slider Cart embed is on, the slider cart replaces all three: `triggerCartRefresh()` re-reads the slider cart without dispatching the events below, `openCartDrawer()` opens the slider cart, and `refreshCartDrawer()` re-reads it and opens it when the slider cart is set to open after an add.

Without the slider cart, `triggerCartRefresh()` dispatches each of these events on `document` and on `document.body`, with `bubbles: true` and `event.detail` set to `{ data: {} }`:

`cart:refresh`, `cart:updated`, `cart-update`, `cart:change`, `cart:update`, `theme:cart:change`, `theme:cart:update` and `dispatch:cart-drawer:open`.

When jQuery is on the page, it also triggers `cart.requestComplete` and `cart:updated` on `$(document)`. Many themes listen for one of these names, so their cart refreshes without code from you.

Bundles widgets also dispatch `cart:refresh`, and some `cart:updated`, after they change the cart. They use `window`, `document` or `document.documentElement`, each with a different `event.detail` or none. All of them reach a listener on `window`, so listen there, and don't rely on `event.detail`.

## Slider cart

The slider cart is the cart drawer that the **Appstle Slider Cart** app embed adds to a theme. These interfaces exist only when that embed is on.

### Slider cart events

The slider cart dispatches these events on `document`. Every `event.detail` includes `timestamp`, in milliseconds since the epoch.

| Event | `event.detail` |
| - | - |
| `appstle-slider-cart:open` | `cartOpenSource` (string, when known) and, for opens through `openCart()`, the cart icon or the sticky cart button, `refetchCart` (boolean) |
| `appstle-slider-cart:close` | `timestamp` only |
| `appstle-slider-cart:cart-updated` | `cart`, each time the cart's contents change |
| `appstle-slider-cart:item-added` | `item`, once per added item |
| `appstle-slider-cart:item-removed` | `item`, once per removed item |
| `appstle-slider-cart:checkout-started` | `cart`, just before the shopper goes to checkout |

`cart` has the shape of Shopify's `/cart.js` response, and `item` the shape of one of its `items`, with money in cents.

On the cart page, closing the drawer takes the shopper to the last store page they visited in that browser tab, or to the store's home page, and no `close` event fires.

To change the drawer's language without a reload, dispatch `appstle-slider-cart:locale-changed` on `document` with `detail.locale` set to a locale your store publishes, for example `fr`. The drawer's built-in labels, such as the subtotal and the checkout button, switch to that language.

### `window.appstleSliderCartApi`

The slider cart sets `window.appstleSliderCartApi`.

* `openCart(options)`: opens the drawer. Every option is optional: `refetchCart` (boolean) re-reads the cart as the drawer opens, `source` (string) becomes the open event's `cartOpenSource`, and `focusReturnAnchor` (element) gets focus back when the drawer closes.
* `closeCart()`: closes the drawer.
* `refreshCart()`: re-reads the cart.
* `getCartData()`: returns the cart the drawer last read.
* `updateTieredPromotionsBar()`: re-reads the cart and updates the tiered promotions bar. Call it after your own code changes the cart. It does nothing when the bar is off.
* `setCheckoutButtonClickHandler(handler)`: makes the checkout button call `handler(cart)` and wait for it. Return `false` to stop the checkout, for example when your own validation fails. Pass `null` to remove the handler.
* `getCheckoutButtonClickHandler()`: returns the current handler.

```javascript theme={null}
const api = window.appstleSliderCartApi;
api.setCheckoutButtonClickHandler(async (cart) => {
  const ok = await validateMyGiftMessage(cart);
  if (!ok) return false;
});
```

## Next steps

> [card] [Build your own frontend](https://developers.appstle.com/bundles/headless-overview.md)
>
> Read bundle configuration and add bundles to the cart from your own storefront.

> [card] [API changelog](https://developers.appstle.com/bundles/api-changelog.md)
>
> Dated changes to the Bundles APIs and JavaScript events.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.