> ## 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.

# Hooks

> Change how the Aftersell Cart behaves: transform lines, enrich them with Storefront data, shape subscription options, and control add-to-cart.

Where [events](/aftersell/cart/sdk-events) let you *react* to the cart and [actions](/aftersell/cart/sdk-actions) let you *change* it, **hooks** change how the cart itself behaves: how lines render, what data they carry, and what happens on add-to-cart.

Hooks live under `window.aftersell.cart.hooks`.

<Note>
  A hook changes what the shopper **sees**; an action changes what's **in their cart**. Hiding a free gift line with a transform leaves it in the cart and in the total. Removing it with [`removeItem`](/aftersell/cart/sdk-actions#removeitemkey) takes it out for real.
</Note>

<Note>
  Hooks are set-up calls, so they're safe to register at the very top of your script, with no need to wait for `ready()`. Register them in your cart's **Initialization** script (see [Custom scripts](/aftersell/cart/custom-scripts)).
</Note>

## How registration works

Every hook is a `register*` method. You call it with your function; it returns an **unregister function** you can call to remove yours.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const off = window.aftersell.cart.hooks.registerSkipAddToCartRule((form) =>
  form.hasAttribute('data-skip-aftersell')
);

// later: off();
```

Registration is **additive**, so your function runs alongside every other one. That matters because your script is rarely the only one on the page: a subscription app, a bundle app, and the theme itself may all register against the same hook. None of them can replace yours, and nothing you register can be silently dropped by whatever loads after you.

| Hook                                                                                      | What it does                                                                                                  | With several registrations                  |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| [`registerLineTransform`](#registerlinetransform)                                         | Hide or relabel individual lines.                                                                             | All run, in registration order.             |
| [`registerLineComparator`](#registerlinecomparator)                                       | Reorder the rendered lines.                                                                                   | Compose as tie-breakers.                    |
| [`registerCartEnricher`](#registercartenricher)                                           | Attach extra Storefront data to each line.                                                                    | All run; each `id` is its own namespace.    |
| [`registerSubscriptionOptionsTransform`](#registersubscriptionoptionstransform)           | Hide or rename a line's selling plans.                                                                        | All run; patches merge per plan, per field. |
| [`registerDefaultSubscriptionOptionSelector`](#registerdefaultsubscriptionoptionselector) | Choose which plan is preselected.                                                                             | First non-`null` answer wins.               |
| [`registerSkipAddToCartRule`](#registerskipaddtocartrule)                                 | Let specific forms bypass the cart. See [Add-to-cart interception](/aftersell/cart/add-to-cart-interception). | Any rule returning `true` skips.            |

A hook that throws, or that isn't a function, is skipped; the rest still run, and the cart carries on. One broken integration can't take down add-to-cart, the subscription picker, or the sort.

The flip side is that a broken hook of yours fails **silently**: nothing reaches the browser console. See [Debugging](/aftersell/cart/sdk-overview#debugging) for where those failures do surface.

***

## registerLineTransform

`registerLineTransform(fn)` runs for every cart line before it renders. Use it to hide a line or change how it reads, without touching what's actually in the shopper's cart.

The function receives a read-only line plus setters. It returns an unregister function.

| Setter                            | Effect                                                                                                                        |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `setHidden(bool)`                 | Hide the line from the drawer. It stays in the cart and in the total.                                                         |
| `setTitle(string)`                | Change the displayed title.                                                                                                   |
| `setVariantTitle(string \| null)` | Change the displayed variant label.                                                                                           |
| `setInternalProperties(obj)`      | Merge render-only properties. Never persisted to Shopify. Used to [group bundle lines](/aftersell/cart/sdk-use-case-bundles). |

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// Hide free gift lines from the drawer. The cart total is unaffected.
const off = window.aftersell.cart.hooks.registerLineTransform((line) => {
  if (line.finalLinePrice === 0) {
    line.setHidden(true);
  }
  if (line.sellingPlan) {
    line.setVariantTitle(`Delivered ${line.sellingPlan.name.toLowerCase()}`);
  }
});

// later: off();
```

<Warning>
  A transform only changes what renders. It can't change price, quantity, or line identity. Use the [actions](/aftersell/cart/sdk-actions) for that.
</Warning>

**Use it for:** hiding gift-with-purchase or app-injected lines, relabelling subscription lines, tagging discounted items, hiding bundle components the shopper shouldn't manage individually.

`setInternalProperties` is the setter behind bundle grouping: stamping the canonical bundle properties onto each line is how you make a third-party app's separate cart lines render as one item. See [Group bundle lines from another app](/aftersell/cart/sdk-use-case-bundles).

## registerLineComparator

A comparator in the same shape `Array.prototype.sort` expects. It runs after hide and rename, so it sees the transformed lines.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// Subscriptions first, then everything else.
window.aftersell.cart.hooks.registerLineComparator((lineA, lineB) => {
  return (lineB.sellingPlan ? 1 : 0) - (lineA.sellingPlan ? 1 : 0);
});
```

Comparators **compose as tie-breakers**: the first one to return a non-zero value decides that pair, and the rest are consulted only on ties. Return `0` for pairs you have no opinion about. That's what hands the decision to the next comparator instead of forcing an order on it.

**Use it for:** floating subscriptions or high-value items to the top, sinking free gifts and add-ons to the bottom, keeping a sponsored product first.

## registerCartEnricher

`registerCartEnricher(registration)` fetches extra product or variant data from the Shopify Storefront API and attaches it to each matching cart line at `line.metadata[id]`. Use it to surface metafields, tags, or anything else the Storefront API exposes, with no code change from Aftersell required.

| Field      | Type                              | Description                                                                                                                     |
| ---------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `id`       | `string`                          | Namespace for the result; it lands at `line.metadata[id]`. Must be unique; a second registration with the same `id` is ignored. |
| `onType`   | `'Product'` or `'ProductVariant'` | Which node the fragment targets. Also the join key (product ID vs. variant ID).                                                 |
| `fragment` | `string`                          | A GraphQL field selection (no outer braces) spliced into the Storefront query. Braces must balance.                             |

Returns an **unregister function**.

Whenever the cart loads or changes, Aftersell fetches your fragment for every product or variant on the cart and attaches the result. The fetch is non-blocking: the cart renders immediately and re-emits `cart_updated` once the data lands. A slow or failing fragment never delays or breaks the cart.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerCartEnricher({
  id: 'pricing',
  onType: 'ProductVariant',
  fragment: `
    anchorPrice: metafield(namespace: "custom", key: "anchor_price") { value }
    subscriberPrice: metafield(namespace: "custom", key: "subscriber_price") { value }
  `,
});

// Read it once the data arrives.
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items.forEach((line) => {
    const anchor = line.metadata.pricing?.anchorPrice;
    if (anchor) console.log(line.title, 'anchor price', anchor.value);
  });
});
```

Because enrichment is asynchronous, always guard the read, since `line.metadata.pricing` is `undefined` until the first fetch resolves, and `metadata` itself defaults to `{}`.

**Use it for:** pulling a metafield onto every line (a delivery estimate, an ingredient list, a "ships separately" flag, a loyalty multiplier) and rendering it through a [Custom code block](/aftersell/cart/custom-code-blocks). See [showing metafield data on cart lines](/aftersell/cart/sdk-use-case-metafields).

<Note>
  Multiple enrichers coexist happily, since each `id` is its own namespace, so their data never collides.
</Note>

<Warning>
  Enriched values are returned as-is from the Storefront API and are **not** sanitized. Render them as text, not as raw HTML.
</Warning>

## registerSubscriptionOptionsTransform

Hide or rename the selling plans offered on a line. Your function receives read-only options plus setters, and returns nothing.

| Setter            | Effect                          |
| ----------------- | ------------------------------- |
| `setHidden(bool)` | Hide the plan from the picker.  |
| `setName(string)` | Change the displayed plan name. |

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerSubscriptionOptionsTransform((options, context) => {
  // context: { productId, variantId }
  options.forEach((option) => {
    if (option.discountPercent === 0) option.setHidden(true);
    option.setName(option.name.replace('Every ', ''));
  });
});
```

**Setters, not a returned list, so that several scripts can coexist.** If this hook returned an array, a transform that only cared about one plan would naturally write `options.filter(...)` and silently delete every other app's plans on its way out. With setters you can only describe your own edits: patches merge per plan and per field, and the last writer wins a genuine conflict on the same field of the same plan. A transform that throws contributes nothing, and the others still apply.

Every transform sees the *original* options, not a half-patched view, so registration order doesn't change what you're reading.

<Note>
  Plan order stays as Shopify returned it, so a transform can't reorder. To control which plan is offered first (and which one the one-time upgrade button subscribes to), use [`registerDefaultSubscriptionOptionSelector`](#registerdefaultsubscriptionoptionselector), which promotes its pick to the front.
</Note>

You also can't *add* a plan or change a price: `discountPercent` has no setter, because a plan Shopify won't honor at checkout would just be a broken promise in the picker.

## registerDefaultSubscriptionOptionSelector

Choose which plan is preselected on a line. Return a plan `id`, or `null` to pass on it.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerDefaultSubscriptionOptionSelector((options) => {
  const best = options
    .slice()
    .sort((optionA, optionB) => optionB.discountPercent - optionA.discountPercent)[0];
  return best ? best.id : null;
});
```

The **first selector to return the id of an available plan wins**, so return `null` for the lines you don't care about rather than guessing. That hands the decision to the next selector instead of overriding it. An id that doesn't match any plan on the line is treated the same as `null` and defers too, so a stale id can't blank out the picker.

Your function receives `(options, context)`, the same `context` the options transform gets.

## registerSkipAddToCartRule

Return `true` to let a specific product form add to the cart normally, bypassing Aftersell entirely. This is useful for a form that needs its own redirect or handling.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerSkipAddToCartRule((form) =>
  form.hasAttribute('data-skip-aftersell')
);
```

**Any `true` skips**, so keep your rule narrow, matching the specific forms you own, and return `false` for everything else. Rules are evaluated in registration order and stop at the first `true`, so don't put side effects in one: whether yours runs at all depends on what registered before it.

<Tip>
  If you control the form's markup, you don't need a hook at all: add the class **`aftersell-cart-skip-atc`** to the `<form>` and Aftersell leaves it alone. Use this hook when you can't edit the markup, or when the decision depends on something only your code knows.
</Tip>

**Use it for:** a pre-order or quote form that needs its own redirect, a subscription app's custom flow, a "buy it now" button that should go straight to checkout. To turn interception off for the whole page instead, use [`skip_add_to_cart_interceptor`](/aftersell/cart/sdk-configure#skip_add_to_cart_interceptor), but prefer this hook, which is scoped to the forms you name.

## Where to go next

* **[Cart object](/aftersell/cart/sdk-cart-object)**: the shape of the line a transform receives.
* **[Events](/aftersell/cart/sdk-events)**: everything you can subscribe to.
* **[Actions](/aftersell/cart/sdk-actions)**: reading and changing the cart.
* **[Use cases](/aftersell/cart/sdk-use-cases)**: complete solutions to common requests.
