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

# Group bundle lines from another app

> Use setInternalProperties to tell the Aftersell Cart which lines belong to the same bundle, so they render as one item instead of several unrelated lines.

Most bundle apps build a bundle by adding **each component as its own cart line**, then linking them with line item properties of their own design. Shopify's Ajax API hands those lines to the cart with no indication that they belong together, so by default the drawer shows a three-part bundle as three unrelated items, each with its own price and quantity stepper.

`setInternalProperties` is how you tell the cart they're one thing.

## How grouping works

The cart groups lines on two **canonical properties**. It doesn't know your bundle app's property names, so you translate: read whatever the app wrote, and stamp the canonical pair onto each line with a [line transform](/aftersell/cart/sdk-hooks#registerlinetransform).

| Property                      | Required | Value                                                       |
| ----------------------------- | -------- | ----------------------------------------------------------- |
| `_aftersell_cart_bundle_id`   | Yes      | A shared ID. Every line carrying the same ID is one bundle. |
| `_aftersell_cart_bundle_role` | No       | Set to `parent` on the line the bundle should display as.   |

These go through `setInternalProperties`, not through Shopify. They're a **render-only overlay**: they never reach `properties`, are never persisted to Shopify, and never appear on the order.

## Step 1: find out what your app writes

Every bundle app names its properties differently, so start by looking at a real cart. Add a bundle on your storefront, then run this in the browser console:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.actions.getCart().items.forEach((line) => {
  console.log(line.title, line.properties);
});
```

You're looking for a property shared across the bundle's lines. It's usually a hidden property (the name starts with `_`) holding an ID, a reference, or the bundle's name. Something like `_bundle_id`, `_bundle_ref`, or `_parent_id` is typical. Note the exact key, and whether one line is marked as the main product.

## Step 2: map it onto the canonical properties

Paste into **Cart settings → Custom script → Initialization**, replacing the property names with the ones you found:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerLineTransform((line) => {
  const props = line.properties;
  if (!props) return;

  const bundleId = props._bundle_id;
  if (!bundleId) return;

  line.setInternalProperties({
    _aftersell_cart_bundle_id: bundleId,
    // Mark the main product so the bundle renders under it.
    _aftersell_cart_bundle_role: props._bundle_role === 'main' ? 'parent' : 'child',
  });
});
```

That's the whole integration. Once two or more lines share an ID, the cart folds them into one bundle.

<Note>
  If your app doesn't mark a main product, leave `_aftersell_cart_bundle_role` off entirely. The cart picks an anchor for you.
</Note>

## What you get

Once lines are grouped, the anchor line carries a [`bundle` object](/aftersell/cart/sdk-cart-object#bundles) and the drawer renders the bundle as a single item:

* **Children nest under the anchor** instead of appearing as separate rows.
* **Quantity is atomic.** Changing the bundle's quantity scales every member together, using each child's `perAnchorQty` ratio, so a bundle with two of one component keeps that two-to-one relationship.
* **Removal is atomic.** Removing the bundle removes every member line in one request, rather than leaving orphaned components behind.
* **One price row.** What it shows follows the **bundle price** setting on the [Cart items](/aftersell/cart/cart-items-block) block: the total of all members, or the main product's price alone.

## How the anchor is chosen

The anchor is the line the bundle displays as. The cart picks it in this order:

1. The line with `_aftersell_cart_bundle_role` set to `parent`.
2. Otherwise, the **highest-priced** member.
3. Otherwise, the first member in the cart.

The price fallback is usually right, since bundle apps tend to put the discount on the main product. Set the role explicitly when it isn't, for example when the main product is the cheapest item or is free.

## Rules worth knowing

* **A bundle needs at least two lines.** A single line carrying a bundle ID is left alone and renders normally.
* **Shopify native bundles are already handled.** Lines that Shopify itself marks as componentized are skipped by this grouping and adapted automatically. You only need this for apps that add separate lines.
* **The transform runs on every render.** Keep it cheap and free of side effects. Don't call actions or fetch from inside it.
* **Merging is additive.** Your properties merge with any set by another transform. On a genuine conflict over the same key, the last-registered transform wins.
* **Grouping runs after hiding and renaming**, and before sorting. So a line you hide with `setHidden` never becomes part of a bundle, and a [comparator](/aftersell/cart/sdk-hooks#registerlinecomparator) sees the anchor, not the children.

<Warning>
  **Grouped children leave `state.items`.** Once lines are folded into a bundle, only the anchor appears in `getCart().items` and in event payloads; the children move to `anchor.bundle.children`. They also stop counting toward `itemCount`.

  The **cart total is unaffected**, because totals come straight from Shopify. Grouping changes presentation, never what the shopper pays.
</Warning>

## Reading a bundle back

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items.forEach((line) => {
    if (!line.bundle) return;
    console.log(line.title, 'is a bundle of', line.bundle.children.length, 'items:');
    line.bundle.children.forEach((child) => {
      console.log('  ', child.quantity, 'x', child.title);
    });
  });
});
```

To act on a bundle's lines, use `bundle.memberKeys`, which holds the `key` of every member including the anchor.

## Using it for other things

Bundle grouping is what `setInternalProperties` was built for, but the overlay is a general channel for **render-only data you derive from a line**. Anything you put there is readable at `line.internalProperties` and in a [Custom code block](/aftersell/cart/custom-code-blocks), without touching the real cart:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerLineTransform((line) => {
  if (line.properties?._preorder_ship_date) {
    line.setInternalProperties({ _badge: `Ships ${line.properties._preorder_ship_date}` });
  }
});
```

Use it when the value is **derived** and display-only. If the data needs to survive to the order, it belongs in a real line item property, set with a hidden `properties[...]` input on the product form so it reaches Shopify whoever performs the add.

## Where to go next

* **[`registerLineTransform`](/aftersell/cart/sdk-hooks#registerlinetransform)**: the hook this runs through.
* **[Cart object](/aftersell/cart/sdk-cart-object#bundles)**: the shape of `bundle` and its children.
* **[Cart items block](/aftersell/cart/cart-items-block)**: the bundle price setting.
