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

# Хуки

> Изменяйте поведение Aftersell Cart: трансформируйте позиции, обогащайте их данными Storefront, формируйте опции подписки и контролируйте добавление в корзину.

Если [события](/ru/aftersell/cart/sdk-events) позволяют *реагировать* на корзину, а [действия](/ru/aftersell/cart/sdk-actions) позволяют её *изменять*, то **хуки** меняют поведение самой корзины: как отрисовываются позиции, какие данные они несут и что происходит при добавлении в корзину.

Хуки находятся в `window.aftersell.cart.hooks`.

<Note>
  Хук меняет то, что покупатель **видит**; действие меняет то, что находится **в его корзине**. Скрытие позиции бесплатного подарка с помощью трансформации оставляет её в корзине и в итоговой сумме. Удаление её через [`removeItem`](/ru/aftersell/cart/sdk-actions#removeitemkey) убирает её по-настоящему.
</Note>

<Note>
  Хуки — это вызовы настройки, поэтому их безопасно регистрировать в самом начале вашего скрипта, без необходимости ждать `ready()`. Регистрируйте их в скрипте **Initialization** вашей корзины (см. [Пользовательские скрипты](/ru/aftersell/cart/custom-scripts)).
</Note>

<div id="how-registration-works">
  ## Как работает регистрация
</div>

Каждый хук — это метод `register*`. Вы вызываете его со своей функцией; он возвращает **функцию отмены регистрации**, которую можно вызвать, чтобы удалить вашу.

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

// later: off();
```

Регистрация **аддитивна**, поэтому ваша функция работает рядом со всеми остальными. Это важно, потому что ваш скрипт редко бывает единственным на странице: приложение подписок, приложение комплектов и сама тема могут регистрироваться на один и тот же хук. Никто из них не может заменить ваш, и ничто из зарегистрированного вами не может быть молча отброшено тем, что загружается после вас.

| Хук                                                                                       | Что он делает                                                                                                                  | При нескольких регистрациях                            |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| [`registerLineTransform`](#registerlinetransform)                                         | Скрывает или переименовывает отдельные позиции.                                                                                | Все выполняются, в порядке регистрации.                |
| [`registerLineComparator`](#registerlinecomparator)                                       | Переупорядочивает отрисовываемые позиции.                                                                                      | Составляются как разрешение ничьих.                    |
| [`registerCartEnricher`](#registercartenricher)                                           | Прикрепляет дополнительные данные Storefront к каждой позиции.                                                                 | Все выполняются; каждый `id` — своё пространство имён. |
| [`registerSubscriptionOptionsTransform`](#registersubscriptionoptionstransform)           | Скрывает или переименовывает планы продаж позиции.                                                                             | Все выполняются; патчи объединяются по плану, по полю. |
| [`registerDefaultSubscriptionOptionSelector`](#registerdefaultsubscriptionoptionselector) | Выбирает, какой план предварительно выбран.                                                                                    | Побеждает первый ответ, отличный от `null`.            |
| [`registerSkipAddToCartRule`](#registerskipaddtocartrule)                                 | Позволяет отдельным формам обходить корзину. См. [Перехват добавления в корзину](/ru/aftersell/cart/add-to-cart-interception). | Любое правило, вернувшее `true`, пропускает.           |

Хук, который выбрасывает исключение или не является функцией, пропускается; остальные продолжают работать, и корзина работает дальше. Одна сломанная интеграция не может вывести из строя добавление в корзину, селектор подписок или сортировку.

Обратная сторона в том, что ваш сломанный хук отказывает **молча**: ничего не попадает в консоль браузера. См. [Отладка](/ru/aftersell/cart/sdk-overview#debugging) о том, где эти сбои всё же проявляются.

***

<div id="registerlinetransform">
  ## registerLineTransform
</div>

`registerLineTransform(fn)` выполняется для каждой позиции корзины перед её отрисовкой. Используйте его, чтобы скрыть позицию или изменить её отображение, не затрагивая фактическое содержимое корзины покупателя.

Функция получает позицию только для чтения плюс сеттеры. Возвращает функцию отмены регистрации.

| Сеттер                            | Эффект                                                                                                                                                                  |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setHidden(bool)`                 | Скрыть позицию из drawer. Она остаётся в корзине и в итоговой сумме.                                                                                                    |
| `setTitle(string)`                | Изменить отображаемое название.                                                                                                                                         |
| `setVariantTitle(string \| null)` | Изменить отображаемую надпись варианта.                                                                                                                                 |
| `setInternalProperties(obj)`      | Объединить свойства только для отрисовки. Никогда не сохраняются в Shopify. Используется для [группировки позиций комплектов](/ru/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>
  Трансформация меняет только то, что отрисовывается. Она не может изменить цену, количество или идентичность позиции. Для этого используйте [действия](/ru/aftersell/cart/sdk-actions).
</Warning>

**Используйте для:** скрытия позиций подарков за покупку или добавленных приложениями, переименования позиций подписки, пометки товаров со скидкой, скрытия компонентов комплектов, которыми покупатель не должен управлять по отдельности.

`setInternalProperties` — это сеттер, лежащий в основе группировки комплектов: проставление канонических свойств комплекта на каждую позицию — это способ заставить отдельные позиции корзины стороннего приложения отрисовываться как один товар. См. [Группировка позиций комплектов из другого приложения](/ru/aftersell/cart/sdk-use-case-bundles).

<div id="registerlinecomparator">
  ## registerLineComparator
</div>

Компаратор в той же форме, которую ожидает `Array.prototype.sort`. Он выполняется после скрытия и переименования, поэтому видит трансформированные позиции.

```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);
});
```

Компараторы **составляются как разрешение ничьих**: первый, вернувший ненулевое значение, решает для этой пары, а остальные опрашиваются только при ничьих. Возвращайте `0` для пар, о которых у вас нет мнения. Именно это передаёт решение следующему компаратору вместо навязывания ему порядка.

**Используйте для:** поднятия подписок или дорогих товаров наверх, опускания бесплатных подарков и дополнений вниз, удержания спонсируемого товара первым.

<div id="registercartenricher">
  ## registerCartEnricher
</div>

`registerCartEnricher(registration)` запрашивает дополнительные данные о товаре или варианте из Shopify Storefront API и прикрепляет их к каждой соответствующей позиции корзины в `line.metadata[id]`. Используйте его для вывода метаполей, тегов или чего-либо ещё, что предоставляет Storefront API, без необходимости изменений кода со стороны Aftersell.

| Поле       | Тип                                | Описание                                                                                                                                    |
| ---------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`       | `string`                           | Пространство имён для результата; он попадает в `line.metadata[id]`. Должно быть уникальным; вторая регистрация с тем же `id` игнорируется. |
| `onType`   | `'Product'` или `'ProductVariant'` | Узел, на который нацелен фрагмент. Также ключ соединения (ID товара vs. ID варианта).                                                       |
| `fragment` | `string`                           | Выборка полей GraphQL (без внешних фигурных скобок), встраиваемая в запрос Storefront. Скобки должны быть сбалансированы.                   |

Возвращает **функцию отмены регистрации**.

Каждый раз, когда корзина загружается или меняется, Aftersell запрашивает ваш фрагмент для каждого товара или варианта в корзине и прикрепляет результат. Запрос неблокирующий: корзина отрисовывается сразу и повторно излучает `cart_updated`, когда данные приходят. Медленный или сбойный фрагмент никогда не задерживает и не ломает корзину.

```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);
  });
});
```

Поскольку обогащение асинхронно, всегда защищайте чтение, так как `line.metadata.pricing` равно `undefined`, пока не разрешится первый запрос, а `metadata` по умолчанию — `{}`.

**Используйте для:** вытягивания метаполя на каждую позицию (оценка доставки, список ингредиентов, флаг «отправляется отдельно», множитель лояльности) и его отрисовки через [блок Custom code](/ru/aftersell/cart/custom-code-blocks). См. [отображение данных метаполей на позициях корзины](/ru/aftersell/cart/sdk-use-case-metafields).

<Note>
  Несколько обогатителей спокойно сосуществуют, поскольку каждый `id` — это своё пространство имён, поэтому их данные никогда не сталкиваются.
</Note>

<Warning>
  Обогащённые значения возвращаются из Storefront API как есть и **не** санитизируются. Отображайте их как текст, а не как необработанный HTML.
</Warning>

<div id="registersubscriptionoptionstransform">
  ## registerSubscriptionOptionsTransform
</div>

Скрывайте или переименовывайте планы продаж, предлагаемые для позиции. Ваша функция получает опции только для чтения плюс сеттеры и ничего не возвращает.

| Сеттер            | Эффект                           |
| ----------------- | -------------------------------- |
| `setHidden(bool)` | Скрыть план из селектора.        |
| `setName(string)` | Изменить отображаемое имя плана. |

```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 ', ''));
  });
});
```

**Сеттеры, а не возвращаемый список — чтобы несколько скриптов могли сосуществовать.** Если бы этот хук возвращал массив, трансформация, которую интересует только один план, естественным образом написала бы `options.filter(...)` и молча удалила бы планы всех остальных приложений. С сеттерами вы можете описать только собственные правки: патчи объединяются по плану и по полю, и последний записавший побеждает в настоящем конфликте по одному полю одного плана. Трансформация, выбросившая исключение, ничего не вносит, а остальные всё равно применяются.

Каждая трансформация видит *исходные* опции, а не наполовину пропатченное представление, поэтому порядок регистрации не меняет того, что вы читаете.

<Note>
  Порядок планов остаётся таким, каким его вернул Shopify, поэтому трансформация не может переупорядочивать. Чтобы контролировать, какой план предлагается первым (и на какой подписывает кнопка апгрейда разовой покупки), используйте [`registerDefaultSubscriptionOptionSelector`](#registerdefaultsubscriptionoptionselector), который продвигает свой выбор в начало.
</Note>

Вы также не можете *добавить* план или изменить цену: у `discountPercent` нет сеттера, потому что план, который Shopify не подтвердит при оформлении заказа, был бы просто нарушенным обещанием в селекторе.

<div id="registerdefaultsubscriptionoptionselector">
  ## registerDefaultSubscriptionOptionSelector
</div>

Выбирайте, какой план предварительно выбран для позиции. Верните `id` плана или `null`, чтобы пропустить.

```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;
});
```

**Побеждает первый селектор, вернувший id доступного плана**, поэтому возвращайте `null` для позиций, которые вас не интересуют, вместо того чтобы гадать. Это передаёт решение следующему селектору вместо его переопределения. Id, не соответствующий ни одному плану позиции, обрабатывается так же, как `null`, и тоже уступает, поэтому устаревший id не может обнулить селектор.

Ваша функция получает `(options, context)` — тот же `context`, что и трансформация опций.

<div id="registerskipaddtocartrule">
  ## registerSkipAddToCartRule
</div>

Верните `true`, чтобы позволить конкретной форме товара добавлять в корзину обычным образом, полностью обходя Aftersell. Это полезно для формы, которой нужен собственный редирект или обработка.

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

**Любое `true` пропускает**, поэтому держите правило узким, соответствующим конкретным формам, которыми вы владеете, и возвращайте `false` для всего остального. Правила оцениваются в порядке регистрации и останавливаются на первом `true`, поэтому не помещайте в них побочные эффекты: сработает ли ваше вообще, зависит от того, что зарегистрировалось до него.

<Tip>
  Если вы контролируете разметку формы, хук вам вообще не нужен: добавьте класс **`aftersell-cart-skip-atc`** к `<form>`, и Aftersell её не тронет. Используйте этот хук, когда вы не можете редактировать разметку или когда решение зависит от чего-то, что знает только ваш код.
</Tip>

**Используйте для:** формы предзаказа или запроса цены, которой нужен собственный редирект, пользовательского потока приложения подписок, кнопки «купить сейчас», которая должна вести прямо к оформлению заказа. Чтобы вместо этого отключить перехват для всей страницы, используйте [`skip_add_to_cart_interceptor`](/ru/aftersell/cart/sdk-configure#skip_add_to_cart_interceptor), но предпочитайте этот хук, ограниченный названными вами формами.

<div id="where-to-go-next">
  ## Что дальше
</div>

* **[Объект корзины](/ru/aftersell/cart/sdk-cart-object)**: структура позиции, которую получает трансформация.
* **[События](/ru/aftersell/cart/sdk-events)**: всё, на что можно подписаться.
* **[Действия](/ru/aftersell/cart/sdk-actions)**: чтение и изменение корзины.
* **[Сценарии использования](/ru/aftersell/cart/sdk-use-cases)**: полные решения распространённых запросов.
