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

# Integracja niestandardowa

> Wywołuj bezpośrednio Strategies API, aby zasilać niestandardowe doświadczenia w witrynie sklepu — karuzele na stronach produktów, niestandardowe upselle w koszyku i wszędzie tam, gdzie natywne powierzchnie Aftersell nie sięgają.

<div id="overview">
  ## Przegląd
</div>

Gdy ani natywne powierzchnie Aftersell (post-purchase, checkout, Upcart), ani gotowa integracja nie pasują, możesz samodzielnie wywoływać Strategies API ze swojego szablonu Shopify i renderować zwrócone produkty w dowolny sposób.

Wzorzec jest zawsze taki sam: zbuduj ładunek kontekstu z Liquid (dzięki temu atrybuty Shopify, takie jak bieżący produkt, zawartość koszyka i pola klienta, są uzupełniane w momencie renderowania), wyślij go metodą `POST` do `/api/public/strategy/evaluate` i wyrenderuj odpowiedź.

Ta strona opisuje dwa wzorce implementacji:

* **Kontekst PDP** — umieść sekcję na stronach produktów, która wywołuje API z **aktualnie przeglądanym produktem** i renderuje karuzelę zwróconych rekomendacji.
* **Kontekst koszyka** — wyrenderuj blok upsell wewnątrz niestandardowego koszyka, który wywołuje API ze **wszystkimi bieżącymi pozycjami koszyka** i renderuje zwrócone produkty.

Tym, co różni oba wzorce, jest kształt **kontekstu produktu**: pojedynczy produkt na PDP, tablica wszystkich pozycji w koszyku.

***

<div id="what-youll-need">
  ## Czego potrzebujesz
</div>

1. **Twój klucz API Strategii.** W Aftersell przejdź do **Settings → Product Strategy** i na karcie **Security Token** skopiuj swój token (to jest Twój klucz API Strategii).
2. **ID Strategii.** Otwórz Strategię, którą chcesz uruchomić, w edytorze Strategii Aftersell i skopiuj jej ID.
3. **Dostęp do kodu szablonu.** Będziesz dodawać sekcję Liquid (PDP) lub blok (niestandardowy koszyk) do swojego szablonu Shopify — Online Store → Themes → ... → Edit code.

<Warning>
  Twój klucz API Strategii znajduje się w kodzie szablonu po stronie klienta, co sprawia, że jest widoczny dla każdego, kto przegląda źródło strony. Traktuj go jak publiczne dane uwierzytelniające witryny sklepu i zrotuj go w Aftersell w **Settings → Product Strategy**, jeśli kiedykolwiek zostanie ujawniony w sposób, którego nie planowałeś.
</Warning>

***

<div id="pdp-context-section-snippet">
  ## Kontekst PDP: snippet sekcji
</div>

Ten wzorzec dodaje sekcję Shopify do Twojej strony produktu. Podczas renderowania strony Liquid osadza w ładunku atrybuty bieżącego produktu, koszyka i klienta, a następnie JavaScript wysyła żądanie do Strategies API i renderuje zwrócone produkty w karuzeli Splide.

<div id="installing">
  ### Instalacja
</div>

1. W panelu administracyjnym Shopify przejdź do **Online Store → Themes**, kliknij **...** przy swoim szablonie i wybierz **Edit code**.
2. W folderze **Sections** utwórz nowy plik o nazwie `aftersell-upsell-carousel.liquid`.
3. Wklej poniższy snippet do nowego pliku i zastąp `YOUR_STRATEGY_API_KEY` kluczem API z Aftersell.
4. Zapisz.
5. Otwórz szablon produktu (zazwyczaj `templates/product.json` lub `sections/main-product.liquid`) i dodaj sekcję **Aftersell Carousel** w miejscu, w którym ma się pojawić karuzela. Z poziomu edytora szablonu możesz też przeciągnąć ją bezpośrednio na stronę produktu.
6. W ustawieniach sekcji wklej swoje **ID Strategii**.

<div id="what-the-section-sends">
  ### Co wysyła sekcja
</div>

Dla każdego wyświetlenia PDP ładunek zawiera:

* **products** — jednoelementową tablicę zawierającą aktualnie przeglądany produkt (productId, variantId, quantity, price, handle, title, vendor, productType, tags, collections, sellingPlan).
* **cart** — sumę częściową, liczbę sztuk, liczbę pozycji bieżącego koszyka kupującego (pomijane, jeśli koszyk jest pusty).
* **cartToken** — dzięki temu API może powiązać tę ocenę z tą samą sesją.
* **customer** — tagi, kraj, prowincję, ustawienia regionalne, liczbę zamówień, łączne wydatki i flagę zgody na marketing, ale **tylko jeśli kupujący jest zalogowany**.
* **session** — kod waluty z `shop.currency`.

Sekcja domyślnie nie wysyła parametrów UTM. Jeśli chcesz targetowania opartego na UTM na PDP, przechwyć je po stronie klienta i dodaj do obiektu `session` przed wywołaniem fetch.

<div id="the-snippet">
  ### Snippet
</div>

```liquid theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
{% comment %}
  Aftersell Carousel (Splide)
  Type: Section — save to sections/aftersell-upsell-carousel.liquid
{% endcomment %}

{% if product %}

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide-core.min.css">
<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js" defer></script>

<div id="aftersell-upsell-{{ section.id }}" class="aftersell-upsell-carousel" style="display:none" {{ section.shopify_attributes }}>
  <h2 class="aftersell-heading">{{ section.settings.heading | default: 'You might also like' }}</h2>
  <div class="aftersell-carousel-wrapper">
    <button class="aftersell-arrow aftersell-arrow--prev" aria-label="Previous" disabled>&#8592;</button>
    <div class="aftersell-track-container">
      <div class="splide" id="aftersell-splide-{{ section.id }}">
        <div class="splide__track">
          <ul class="splide__list">
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
          </ul>
        </div>
      </div>
    </div>
    <button class="aftersell-arrow aftersell-arrow--next" aria-label="Next" disabled>&#8594;</button>
  </div>
</div>

<style>
.aftersell-upsell-carousel { font-family: Modernist, sans-serif; max-width: 1300px; margin: 0 auto; padding: 24px 0 0; box-sizing: border-box; }
.aftersell-heading { font-family: Modernist, sans-serif; font-size: 30px; font-weight: 700; line-height: 45px; color: #0C0A09; margin: 0; }
.aftersell-carousel-wrapper { display: flex; align-items: center; gap: 8px; }
.aftersell-track-container { overflow: hidden; flex: 1; min-width: 0; }
.aftersell-card { display: flex; flex-direction: column; box-sizing: border-box; height: 100%; }
.aftersell-card-link { text-decoration: none; color: inherit; display: block; flex: 1; }
.aftersell-card-image { aspect-ratio: 1; overflow: hidden; background: #f5f5f5; border-radius: 8px 8px 0 0; position: relative; }
.aftersell-card-image img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.3s ease; }
.aftersell-card-image:hover img { transform: scale(1.04); }
.aftersell-card-badge { position: absolute; top: 10px; left: 8px; background: #c60006; color: #fff; font-size: 11px; font-weight: 500; padding: 4px 8px; border-radius: 16px; z-index: 1; }
.aftersell-card-body { padding: 6px 0 0; display: flex; flex-direction: column; }
.aftersell-card-vendor { font-size: 13px; font-weight: 550; text-transform: uppercase; color: #1d4481; margin: 0; }
.aftersell-card-title { font-size: 13px; font-weight: 700; color: #0C0A09; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aftersell-card-price { font-size: 13px; color: #0C0A09; margin: 0; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.aftersell-card-price--compare { text-decoration: line-through; color: #595959; }
.aftersell-card-price--sale { font-weight: 700; color: #c60006; }
.aftersell-card-actions { padding: 6px 0 0; display: flex; flex-direction: column; margin-top: auto; }
.aftersell-variant-select { width: 100%; font-size: 13px; padding: 0.35rem 0.5rem; border: 1px solid #DBDBDB; border-radius: 4px; background: #fff; color: #0C0A09; cursor: pointer; }
.aftersell-cta { padding: 16px 12px; background: #c50007; color: #FFFFFF; border: none; border-radius: 5px; font-size: 16px; font-weight: 700; cursor: pointer; width: 100%; display: flex; align-items: center; justify-content: center; transition: background 0.2s, opacity 0.2s; }
.aftersell-cta:hover:not(:disabled) { opacity: 0.85; }
.aftersell-cta:disabled { opacity: 0.5; cursor: default; }
.aftersell-cta--added { background: #1d4481; }
.aftersell-cta--unavailable { background: #999; cursor: default; }
.aftersell-arrow { width: 30px; height: 90px; background: rgba(255,255,255,0.6); border: 1px solid #DBDBDB; border-radius: 5px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; padding: 0; }
.aftersell-arrow:disabled { opacity: 0.3; cursor: default; }
.aftersell-skeleton { border-radius: 8px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: aftersell-shimmer 1.4s infinite; aspect-ratio: 0.75; width: 100%; }
@keyframes aftersell-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

#aftersell-splide-{{ section.id }} .splide__arrows,
#aftersell-splide-{{ section.id }} .splide__pagination { display: none !important; }

@media (max-width: 768px) {
  .aftersell-upsell-carousel { padding: 16px 16px 0; }
  .aftersell-arrow { display: none; }
  .aftersell-carousel-wrapper { gap: 0; }
}
</style>

<script>
(function() {
  var BACKEND_URL  = 'https://start.aftersell.app';
  var API_KEY      = 'YOUR_STRATEGY_API_KEY';
  var STRATEGY_ID  = {{ section.settings.strategy_id | json }};
  var SHOP_DOMAIN  = {{ shop.permanent_domain | json }};
  var CTA_LABEL    = '{{ section.settings.cta_label | default: "Add to cart" }}';
  var MAX_PRODUCTS = {{ section.settings.max_products | default: 8 }};
  var CURRENCY     = {{ shop.currency | default: "USD" | json }};
  var SECTION_ID   = {{ section.id | json }};
  if (!SHOP_DOMAIN || !STRATEGY_ID) return;

  var fmt;
  try {
    fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: CURRENCY });
  } catch(e) {
    fmt = { format: function(n) { return '$' + parseFloat(n).toFixed(2); } };
  }
  function money(str) { return fmt.format(parseFloat(str) || 0); }

  function gidToNumeric(gid) {
    return gid ? String(gid).split('/').pop() : null;
  }

  function priceHtml(price, compareAt) {
    var hasSale = compareAt && parseFloat(compareAt) > parseFloat(price);
    return hasSale
      ? '<span class="aftersell-card-price--compare">' + money(compareAt) + '</span>'
        + '<span class="aftersell-card-price--sale">' + money(price) + '</span>'
      : '<span class="aftersell-card-price--current">' + money(price) + '</span>';
  }

  var productContext = {
    productId:   'gid://shopify/Product/{{ product.id }}',
    variantId:   'gid://shopify/ProductVariant/{{ product.selected_or_first_available_variant.id }}',
    quantity:    1,
    price:       {{ product.price | divided_by: 100.0 }},
    handle:      {{ product.handle | json }},
    title:       {{ product.title | json }},
    vendor:      {{ product.vendor | json }},
    productType: {{ product.type | json }},
    tags:        {{ product.tags | json }},
    collections: [{% for col in product.collections %}'gid://shopify/Collection/{{ col.id }}'{% unless forloop.last %},{% endunless %}{% endfor %}],
    sellingPlan: {% if product.selected_selling_plan %}'subscription'{% else %}'one-time'{% endif %}
  };

  var cartContext = {
    subtotal:  {{ cart.total_price | divided_by: 100.0 }},
    itemCount: {{ cart.item_count }},
    lineCount: {{ cart.items.size }}
  };

  {% if customer %}
  var customerContext = {
    customerId:       'gid://shopify/Customer/{{ customer.id }}',
    tags:             {{ customer.tags | json }},
    {% if customer.default_address.country_code %}countryCode: {{ customer.default_address.country_code | json }},{% endif %}
    {% if customer.default_address.province_code %}provinceCode: {{ customer.default_address.province_code | json }},{% endif %}
    locale:           {{ request.locale.iso_code | json }},
    orderCount:       {{ customer.orders_count }},
    totalSpent:       {{ customer.total_spent | times: 1.0 }},
    acceptsMarketing: {{ customer.accepts_marketing }}
  };
  {% endif %}

  var blockEl  = document.getElementById('aftersell-upsell-' + SECTION_ID);
  var splideEl = document.getElementById('aftersell-splide-' + SECTION_ID);
  var list     = splideEl.querySelector('.splide__list');
  var prevBtn  = blockEl.querySelector('.aftersell-arrow--prev');
  var nextBtn  = blockEl.querySelector('.aftersell-arrow--next');
  var splideInstance = null;

  function getCartToken() {
    var t = {{ cart.token | json }};
    if (t) return Promise.resolve(t);
    return fetch('/cart.js').then(function(r){ return r.json(); }).then(function(c){ return c.token || null; }).catch(function(){ return null; });
  }

  function evaluate() {
    getCartToken().then(function(cartToken) {
      if (!cartToken) return;
      return fetch(BACKEND_URL + '/api/public/strategy/evaluate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Strategy-Api-Key': API_KEY },
        body: JSON.stringify({
          shopDomain:  SHOP_DOMAIN,
          strategyId:  STRATEGY_ID,
          context: {
            products:  [productContext],
            cartToken: cartToken,
            cart:      cartContext.itemCount > 0 ? cartContext : undefined,
            {% if customer %}customer: customerContext,{% endif %}
            session:   { currencyCode: CURRENCY }
          }
        })
      });
    }).then(function(res) { if (!res) return; return res.json(); })
    .then(function(data) {
      if (!data || !data.success || !data.products || !data.products.length) {
        clearSkeletons(); return;
      }
      renderCards(data.products.slice(0, MAX_PRODUCTS));
    }).catch(function(err) {
      console.error('[AfterSell carousel] Error:', err);
      clearSkeletons();
    });
  }

  function renderCards(products) {
    if (!products || !products.length) {
      blockEl.style.display = 'none'; return;
    }

    blockEl.style.display = '';

    list.innerHTML = products.map(function(p, i) {
      var img      = p.images && p.images[0] ? p.images[0].src : '';
      var alt      = (p.images && p.images[0] && p.images[0].altText) || p.title;
      var variants = p.variants || [];
      var availableVariants = variants.filter(function(v) {
        return v.availableForSale !== false;
      });
      var isDefaultVariant = variants.length === 1 && variants[0].title === 'Default Title';
      var showSelect       = !isDefaultVariant && availableVariants.length >= 1;
      var firstVariant     = availableVariants.length ? availableVariants[0] : null;
      var firstVariantId   = firstVariant ? gidToNumeric(firstVariant.variantId) : null;
      var anyAvailable     = availableVariants.length > 0;

      var onSale = p.compareAtPrice && parseFloat(p.compareAtPrice) > parseFloat(p.price);
      var badge  = '';
      if (onSale) {
        var pct = Math.round((1 - parseFloat(p.price) / parseFloat(p.compareAtPrice)) * 100);
        badge = '<span class="aftersell-card-badge">Save ' + pct + '%</span>';
      }
      var vendor = p.vendor ? '<p class="aftersell-card-vendor">' + p.vendor + '</p>' : '';

      var selectHtml = '';
      if (showSelect) {
        var options = availableVariants.map(function(v) {
          return '<option value="' + gidToNumeric(v.variantId) + '"'
            + ' data-price="'   + (v.price         || p.price) + '"'
            + ' data-compare="' + (v.compareAtPrice || '')     + '"'
            + '>' + v.title + '</option>';
        }).join('');
        selectHtml = '<select class="aftersell-variant-select" aria-label="Select variant">' + options + '</select>';
      }

      var atcLabel = anyAvailable ? CTA_LABEL : 'Sold Out';
      var atcClass = anyAvailable ? 'aftersell-cta' : 'aftersell-cta aftersell-cta--unavailable';
      var atcBtn   = '<button class="' + atcClass + '"'
        + (firstVariantId ? ' data-variant-id="' + firstVariantId + '"' : '')
        + (anyAvailable ? '' : ' disabled')
        + '>' + atcLabel + '</button>';

      var productUrl = p.url + (p.url.indexOf('?') > -1 ? '&' : '?') + 'ref=aftersell';

      return '<li class="splide__slide">'
        + '<div class="aftersell-card" data-idx="' + i + '">'
        + '<a class="aftersell-card-link" href="' + productUrl + '">'
        + '<div class="aftersell-card-image">' + badge
        + (img ? '<img src="' + img + '" alt="' + alt + '" loading="lazy">' : '')
        + '</div>'
        + '<div class="aftersell-card-body">'
        + vendor
        + '<p class="aftersell-card-title">' + p.title + '</p>'
        + '<p class="aftersell-card-price" data-price-el>' + priceHtml(p.price, p.compareAtPrice) + '</p>'
        + '</div></a>'
        + '<div class="aftersell-card-actions">'
        + selectHtml
        + atcBtn
        + '</div></div></li>';
    }).join('');

    attachCardEvents();
    initSplide();
  }

  function attachCardEvents() {
    list.addEventListener('change', function(e) {
      if (!e.target.classList.contains('aftersell-variant-select')) return;
      var select  = e.target;
      var card    = select.closest('.aftersell-card');
      var opt     = select.options[select.selectedIndex];
      var priceEl = card.querySelector('[data-price-el]');
      var btn     = card.querySelector('.aftersell-cta');
      priceEl.innerHTML     = priceHtml(opt.getAttribute('data-price'), opt.getAttribute('data-compare'));
      btn.dataset.variantId = opt.value;
      btn.disabled          = false;
      btn.textContent       = CTA_LABEL;
      btn.className         = 'aftersell-cta';
    });

    list.addEventListener('click', function(e) {
      var btn = e.target.closest('.aftersell-cta');
      if (!btn || btn.disabled) return;
      var variantId = btn.dataset.variantId;
      if (!variantId) return;

      e.preventDefault();
      btn.disabled    = true;
      btn.textContent = 'Adding…';

      fetch('/cart/add.js', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          id: parseInt(variantId, 10),
          quantity: 1,
          properties: {
            "_source": "Aftersell",
            "_attribution": "CTA"
          }
        })
      })
      .then(function(r) {
        if (!r.ok) throw new Error('Cart add failed: ' + r.status);
        return r.json();
      })
      .then(function() {
        btn.textContent = 'Added!';
        btn.classList.add('aftersell-cta--added');
        if (typeof window.theme !== 'undefined' && window.theme.cart && window.theme.cart.open) {
          window.theme.cart.open();
        }
        setTimeout(function() {
          btn.textContent = CTA_LABEL;
          btn.classList.remove('aftersell-cta--added');
          btn.disabled = false;
        }, 2500);
      })
      .catch(function(err) {
        console.error('[AfterSell carousel] Add to cart error:', err);
        btn.textContent = 'Try Again';
        btn.disabled    = false;
      });
    });
  }

  function initSplide() {
    function mount() {
      if (typeof window.Splide === 'undefined') {
        setTimeout(mount, 50);
        return;
      }

      splideInstance = new Splide('#aftersell-splide-' + SECTION_ID, {
        type:       'slide',
        perPage:    4,
        perMove:    4,
        gap:        '10px',
        pagination: false,
        arrows:     false,
        speed:      350,
        drag:       false,
        breakpoints: {
          768: { perPage: 2, perMove: 2, drag: 'free', snap: true },
          480: { perPage: 1, perMove: 1, drag: 'free', snap: true, padding: { right: '25%' } }
        }
      }).mount();

      function updateArrows() {
        var idx = splideInstance.index;
        var end = splideInstance.length - splideInstance.options.perPage;
        prevBtn.disabled = idx <= 0;
        nextBtn.disabled = idx >= end;
      }

      prevBtn.addEventListener('click', function() { splideInstance.go('<'); });
      nextBtn.addEventListener('click', function() { splideInstance.go('>'); });
      splideInstance.on('moved', updateArrows);
      updateArrows();
    }
    mount();
  }

  function clearSkeletons() { list.innerHTML = ''; }

  evaluate();
})();
</script>

{% endif %}

{% schema %}
{
  "name": "Aftersell Carousel",
  "settings": [
    { "type": "text",  "id": "strategy_id",  "label": "AfterSell Strategy ID", "default": "ADD_ID_HERE" },
    { "type": "text",  "id": "heading",      "label": "Heading",               "default": "You might also like" },
    { "type": "text",  "id": "cta_label",    "label": "CTA Button Label",      "default": "Add to cart" },
    { "type": "range", "id": "max_products", "label": "Max Products to Show",  "default": 8, "min": 1, "max": 20, "step": 1 }
  ],
  "presets": [{ "name": "Aftersell Carousel" }]
}
{% endschema %}
```

<Frame>
  <img src="https://mintcdn.com/aftersell/SnVX3h-PpMMxQMDU/images/aftersell/strategy-carousel-example.png?fit=max&auto=format&n=SnVX3h-PpMMxQMDU&q=85&s=8fb7ac08c2ae0c430b5bc4f810c59e40" alt="Karuzela produktów zasilana Strategią wyrenderowana na stronie produktu Shopify" width="2692" height="1146" data-path="images/aftersell/strategy-carousel-example.png" />
</Frame>

<div id="customizing">
  ### Dostosowywanie
</div>

Schema sekcji udostępnia cztery ustawienia edytowalne przez sprzedawcę: **Strategy ID**, **Heading**, **CTA Button Label** i **Max Products to Show**. Dodawaj lub usuwaj ustawienia w bloku `{% schema %}`, aby udostępnić więcej opcji w edytorze szablonu.

CSS jest ograniczony do klas `.aftersell-*` i zawiera napędzaną Splide karuzelę 4-elementową, która przechodzi na 2 elementy przy 768px i 1 element przy 480px. Możesz go dowolnie edytować, aby dopasować do swojego szablonu — nic z tego nie jest wymagane do działania wywołania API.

***

<div id="cart-context-custom-cart-upsell-block">
  ## Kontekst koszyka: niestandardowy blok upsell w koszyku
</div>

Ten wzorzec jest strukturalnie taki sam jak wzorzec PDP, z jedną kluczową różnicą: **tablica kontekstu produktów** jest budowana z pozycji koszyka zamiast z aktualnie przeglądanego produktu. Strategia otrzymuje wtedy każdy produkt dodany przez kupującego i zwraca rekomendacje na podstawie koszyka jako całości.

Implementacja znajduje się tam, gdzie znajduje się kod Twojego niestandardowego koszyka — sekcja Liquid renderująca cart drawer, niestandardowy blok w headlessowej witrynie sklepu lub szablon taki jak `cart.liquid`. Kształt wywołania API i obsługa odpowiedzi są identyczne jak w przykładzie PDP — różni się tylko tablica `products`.

Struktura wygląda tak:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
var products = {{ cart.items | json }}.map(function(item) {
  return {
    productId: 'gid://shopify/Product/' + item.product_id,
    variantId: 'gid://shopify/ProductVariant/' + item.variant_id,
    quantity:  item.quantity,
    price:     item.price / 100
    // ...other fields as needed
  };
});
```

Reszta ładunku (cart, customer, session, cartToken) oraz wywołanie `fetch` do `/api/public/strategy/evaluate` pozostają niezmienione względem powyższego wzorca PDP — jedynie tablica `products` zamienia się z `[productContext]` na tablicę pochodzącą z koszyka.

***

<div id="what-happens-when-the-strategy-returns">
  ## Co się dzieje, gdy Strategia zwraca wynik
</div>

Kształt odpowiedzi jest taki sam niezależnie od tego, który kontekst został wysłany:

```json theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
{
  "success": true,
  "products": [ /* enriched recommended products */ ],
  "evaluationId": "f3a1c2e0-...",
  "resolution": { "strategyId": "...", "matchedRuleIds": [...], "fallbackUsed": false },
  "meta": { "servedFromCache": false, "processingTimeMs": 12, "data": {} }
}
```

`evaluationId` to unikalny identyfikator tej oceny. Jeśli go przechwycisz i dołączysz do renderowanych produktów, możesz przypisać wynikowe zamówienie do dokładnie tej rekomendacji, która je wygenerowała — zobacz sekcję [Atrybucja](#attribution) poniżej.

Sposób renderowania tablicy `products` zależy całkowicie od kodu Twojego szablonu. Powyższy snippet PDP renderuje je jako karuzelę kart z selektorami wariantów i przyciskami dodawania do koszyka; niestandardowy blok koszyka mógłby renderować je jako pionową listę wewnątrz drawera.

Pełny schemat żądania i odpowiedzi znajdziesz w [referencji API Evaluate Strategy](/pl/aftersell/strategies_api_reference_evaluate_strategy).

***

<div id="when-no-product-is-returned">
  ## Gdy żaden produkt nie zostanie zwrócony
</div>

Jeśli Strategia nie zwraca produktów (`products: []`), sposób obsługi zależy od Twojego kodu. Powyższy snippet PDP całkowicie ukrywa karuzelę. Niestandardowy blok koszyka mógłby wrócić do domyślnej listy upselli koszyka albo po prostu nie renderować nic.

Aby uniknąć pustej odpowiedzi, skonfiguruj w Strategii regułę **Catch all**, dzięki której zawsze będzie istniał zapasowy produkt do zwrócenia. Zobacz stronę [Budowanie Strategii](/pl/aftersell/strategies_building_in_app), aby dowiedzieć się, jak skonfigurować Catch all.

***

<div id="tips-for-custom-integrations">
  ## Wskazówki dotyczące integracji niestandardowych
</div>

* **Buduj kontekst w Liquid.** Liquid działa w momencie renderowania i ma dostęp do pełnego grafu obiektów Shopify — product, cart, customer, shop, request. Używaj go do wypełniania ładunku po stronie serwera, zamiast sięgać po wywołania po stronie klienta.
* **Trzymaj klucz API z dala od publicznych repozytoriów.** Trafi on do kodu Twojego szablonu, który jest dostarczany do przeglądarki — to w porządku. Nie wklejaj jednak tego samego szablonu do publicznego repozytorium ani nie udostępniaj paczki na zewnątrz.
* **Używaj Catch all.** Doświadczenia w witrynie sklepu wyglądają na zepsute, gdy slot znika. Catch all z niewielkim zestawem bezpiecznych produktów domyślnych utrzymuje spójność interfejsu.
* **Cache'uj tam, gdzie ma to sens.** Strategies API stosuje lekkie cache'owanie po stronie serwera (`meta.servedFromCache`), ale na PDP o dużym ruchu warto też debounce'ować lub memoizować wywołania po stronie klienta (np. nie wywoływać ponownie, gdy ten sam produkt jest renderowany dwa razy w jednej sesji).

***

<div id="attribution">
  ## Atrybucja
</div>

Gdy kupujący kliknie przycisk dodawania do koszyka w snippecie, wywołanie `/cart/add.js` dołącza do pozycji koszyka **właściwości pozycji (line item properties)**:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: {
  "_source": "Aftersell",
  "_attribution": "CTA"
}
```

Te właściwości podróżują z pozycją aż do zamówienia Shopify, gdzie pojawiają się w rekordzie pozycji. Możesz ich używać dalej do atrybucji przychodu, filtrowania zamówień lub zasilania narzędzi analitycznych, które odczytują właściwości pozycji.

Klucze i wartości to konwencje, a nie wymagania — wywołanie API działa tak samo niezależnie od tego, co tu umieścisz. Zmień je, aby pasowały do Twojego własnego modelu atrybucji. Na przykład:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: {
  "_source": "PDP Carousel",
  "_strategy_id": "{{ section.settings.strategy_id }}",
  "_campaign": "summer-2026"
}
```

<Tip>
  Klucze właściwości zaczynające się od podkreślnika (`_`) są ukryte w interfejsie koszyka i checkoutu, ale nadal są dołączane do zamówienia. Używaj prefiksu podkreślnika dla metadanych przeznaczonych wyłącznie do atrybucji, których kupujący nie mają widzieć.
</Tip>

Zastosuj ten sam wzorzec w implementacji z kontekstem koszyka — każde wywołanie dodania do koszyka z niestandardowego bloku upsell może przenosić dowolne właściwości, jakich potrzebujesz.

<div id="attributing-back-to-the-evaluation">
  ### Atrybucja do konkretnej oceny
</div>

Aby powiązać zamówienie z **dokładną oceną**, która zarekomendowała produkt — a nie tylko z faktem, że „pochodzi ze Strategii” — przechwyć `evaluationId` z odpowiedzi i dołącz go do pozycji pod właściwością `__as_offer_id`. AfterSell odczytuje ten klucz, więc zamówienia nim oznaczone są przypisywane do konkretnej oceny w raportowaniu.

W handlerze `evaluate()` zachowaj identyfikator z odpowiedzi:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
.then(function(data) {
  if (!data || !data.success || !data.products || !data.products.length) {
    clearSkeletons(); return;
  }
  evaluationId = data.evaluationId; // declare `var evaluationId;` alongside your other state
  renderCards(data.products.slice(0, MAX_PRODUCTS));
})
```

Następnie uwzględnij go we właściwościach dodawania do koszyka:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: Object.assign(
  { "_source": "Aftersell", "_attribution": "CTA" },
  evaluationId ? { "__as_offer_id": evaluationId } : {}
)
```

Zachowaj podwójny podkreślnik w `__as_offer_id` — to klucz, którego szuka AfterSell, a prefiks podkreślnika ukrywa go przed kupującymi. Jeśli `evaluationId` nie istnieje (na przykład nie zwrócono żadnych produktów), pomiń tę właściwość, zamiast wysyłać pustą wartość.
