Przejdź do głównej zawartości

Checkout

W DoSwiftly koszyk jest jedynym aggregate realizacji zamówienia — nie ma osobnego obiektu Checkout. Adres dostawy, metoda wysyłki, płatność, kody rabatowe i finalizacja są wystawione bezpośrednio na obiekcie Cart. Storefront prowadzi kupującego przez kolejne kroki tymi samymi metodami koszyka, a cartComplete zamienia koszyk w zamówienie.

Recovery, statusy i pełen lifecycle

Ta strona pokazuje happy path. Obsługę wygasłego koszyka (auto-recovery, event cart-expired), statusy Cart.status, kody błędów dostępu i diagram sekwencji opisuje Koszyk → Cart Completion Lifecycle.

Flow w skrócie

addItem → setShippingAddress → selectShippingMethod
→ (opcjonalnie) applyGiftCard / kod rabatowy
→ selectPaymentMethod → complete → (online) createPayment

Każdy krok to jedna mutacja na koszyku. Wszystkie są dostępne jako metody hooka useCartManager() (zalecane — z auto-recovery i automatycznym czyszczeniem cookie cart-id po complete()). Surowy CartClient daje te same operacje bez warstwy React.

1. Odkrycie metod wysyłki i płatności

Zanim pokażesz picker, pobierz opcje dostępne dla bieżącego koszyka. Sygnatury renderowane są wprost ze schematu (zero driftu):

Dostępne metody wysyłki (cart-aware)

CartAvailableShippingMethodsqueryShipping Methods
query CartAvailableShippingMethods($cartId: ID!, $address: ShippingAddressInput!)

Cart-aware shipping methods discovery. Returns shipping methods available for the cart's contents at the given destination, with subtotal and physical-item weight pulled from the cart aggregate (no need to compute them client-side). When the cart contains only non-physical items (digital, gift card, service, subscription), the response is `methods: []` plus a `DIGITAL_ONLY_NO_SHIPPING` user error — use this as the signal to skip rendering the shipping picker step. Prefer this query over the standalone `AvailableShippingMethods` once a cart has been created (`cartCreate`). For pre-cart "shipping cost preview" UIs on product detail pages, the standalone query remains the right tool.

Variables

NameTypeDefaultRequired
$cartIdID!Yes
$addressShippingAddressInput!Yes
GraphQL operation
query CartAvailableShippingMethods($cartId: ID!, $address: ShippingAddressInput!) {
cart(id: $cartId) {
id
requiresShipping
availableShippingMethods(address: $address) {
methods {
...AvailableShippingMethod
}
freeShippingProgress {
...FreeShippingProgress
}
userErrors {
...UserError
}
}
}
}

Dla koszyka w 100% cyfrowego (DIGITAL / GIFT_CARD / SERVICE / SUBSCRIPTION) zwraca pustą listę methods + userErrors[{ code: 'DIGITAL_ONLY_NO_SHIPPING' }] — użyj tego sygnału (lub pola cart.requiresShipping), żeby pominąć cały krok wysyłki.

Gdy dla adresu nie ma żadnej metody — kraj spoza stref wysyłki sklepu albo kraj objęty sankcjami (Rosja, Białoruś, Korea Północna) — lista methods jest pusta, a userErrors zawiera code: 'NO_SHIPPING_METHODS'. Bez metody wysyłki zamówienia nie da się sfinalizować. Żeby kupujący nie trafiał na ten błąd, wybór kraju dostawy buduj z shop.shipsToCountries (kraje objęte sankcjami są z tej listy pominięte) — patrz Kody krajów.

Dostępne metody płatności

AvailablePaymentMethodsqueryPayment Methods
query AvailablePaymentMethods

Returns the active payment methods for the shop, sorted by the merchant-configured display position. Shop-level — does NOT vary by cart amount or currency. Each method exposes `type` (`CARD`, `BANK_TRANSFER`, `BLIK`, `PAYPAL`, `APPLE_PAY`, `GOOGLE_PAY`, `CASH_ON_DELIVERY`, `OTHER`), provider, icon, description, and supported currencies. Use to render the payment step of checkout.

GraphQL operation
query AvailablePaymentMethods {
availablePaymentMethods {
...AvailablePaymentMethods
}
}
Uses fragments: AvailablePaymentMethods

2. Adres i metoda wysyłki

CartSetShippingAddressmutationCart Completion Mutations
mutation CartSetShippingAddress($input: CartSetShippingAddressInput!)

Phase 3 unify-cart-graphql-surface: wszystkie fulfillment + payment + completion operations teraz na Cart aggregate (zamiast Checkout dual-aggregate). Klient robi typowy checkout flow: cart create/add items → setShipping/Billing/Method → selectPayment → (optional) applyGiftCard → cartComplete → Order created. Sets the shipping address on the cart (full replace, not patch). Triggers cart re-pricing (tax recalculation per address country/region). Address format validated against `CartAddressInput` constraints (firstName/lastName/streetLine1/city/country/postalCode required). Errors: `INVALID_ADDRESS`, `CART_NOT_FOUND`.

CartSelectShippingMethodmutationCart Completion Mutations
mutation CartSelectShippingMethod($input: CartSelectShippingMethodInput!)

Selects a shipping method by `shippingMethodId` (typed `ID!`, a stable shipping-method UUID — NOT a per-request token). The id comes from a list of methods available for the current address + cart subtotal (queryable separately). Errors: `SHIPPING_METHOD_REQUIRED`, `ZIP_CODE_NOT_SUPPORTED`, `CART_NOT_FOUND`.

Adresy używają neutralnego, międzynarodowego nazewnictwa pól (streetLine1, streetLine2, state, stateCode, postalCode) — patrz Konto klienta.

Adres PL (dostawa pod adres) wymaga numeru budynku

Dla adresu polskiego (country: PL) z dostawą pod adres (bez punktu odbioru) buildingNumber jest wymagany w CartSetShippingAddress i CartSetBillingAddress — numer budynku jest potrzebny do doręczenia kurierem oraz na fakturze. Jego brak zwraca userErrors[{ code: 'BUILDING_NUMBER_REQUIRED' }]. Adres z pickupPoint (paczkomat / POP) jest z tego wymogu zwolniony.

Wybór punktu odbioru (paczkomat / POP)

Każda metoda z odpowiedzi availableShippingMethods ma pole deliveryType:

DeliveryTypeenumPełna referencja →

Delivery destination type of a shipping method — signals whether the storefront must collect a pickup point.

WartośćOpis
HOMECourier delivery to the street address — no pickup point required.
LOCKERAutomated parcel locker — the buyer collects from a 24/7 self-service machine.
PICKUP_POINTStaffed pickup point / parcel shop — the buyer collects the parcel at a counter.

Dla metod LOCKER / PICKUP_POINT kupujący musi wybrać konkretny punkt, zanim wybierze metodę. Sposób wyboru opisuje pole pickupConfig:

ShippingPickupConfigobjectPełna referencja →

Pickup-point selection config for a shipping method whose `deliveryType` is LOCKER or PICKUP_POINT. Contains ONLY public, browser-safe data — for InPost this is the domain-scoped Geowidget token, never the ShipX API secret. Use it to render the carrier map (`selectionMode = WIDGET`) or to drive a server-side point search (`selectionMode = SEARCH`).

PoleTypOpis
providerString!Carrier code this config belongs to (e.g. "inpost"). Matches `carrier.serviceCode`'s provider.
scriptUrlStringCDN URL of the carrier widget script to load before rendering the map. Null for SEARCH mode.
selectionModePickupSelectionMode!How to let the buyer pick a point — WIDGET (carrier map) or SEARCH (server-side point lookup).
widgetShippingPickupWidgetEverything needed to mount the carrier map: which widget it is, its script and stylesheet, and the attributes to put on the mount element. Null for SEARCH mode. Prefer this over the flat `widgetToken`/`scriptUrl` fields — they cover only part of what a map needs.
widgetTokenStringPUBLIC widget token used to initialise the carrier map (InPost Geowidget domain-scoped token). Never the carrier API secret. Null for SEARCH mode and when the merchant has not configured the public token (do not render the map — offer SEARCH or hide the method).

selectionMode rozstrzyga, jak renderujesz wybór:

PickupSelectionModeenumPełna referencja →

How the buyer selects a pickup point for a shipping method that requires one.

WartośćOpis
SEARCHNo browser widget — query points server-side (by city / postal code) and render your own list. `widgetToken` / `scriptUrl` are null for this mode.
WIDGETRender the carrier map widget. Everything needed to mount it is in `pickupConfig.widget` (widget kind, script, stylesheet, attributes) — the buyer picks a point on the map.

Tryb WIDGET (mapa punktów przewoźnika)

Mapę montuje za Ciebie hook usePickupPointWidget — ładuje skrypt i arkusz stylów przewoźnika, tworzy właściwy element, podpina odbiór wyboru i sprząta po odmontowaniu.

'use client';
import { useCartManager, usePickupPointWidget } from '@doswiftly/storefront-sdk/react';

// `method` to wybrany AvailableShippingMethod, `address` — adres z formularza kasy.
export function PickupPointPicker({ method, address }) {
const { setShippingAddress, selectShippingMethod } = useCartManager();

const { mountProps, status, error } = usePickupPointWidget({
pickupConfig: method.pickupConfig,
onSelect: async (point) => {
// Najpierw punkt na adresie, dopiero potem metoda — w odwrotnej kolejności
// API odrzuci wybór metody, bo koszyk nie ma jeszcze punktu odbioru.
await setShippingAddress({
...address,
pickupPoint: {
provider: method.pickupConfig.provider,
pointId: point.pointId,
name: point.name,
address: point.address,
},
});
await selectShippingMethod({ shippingMethodId: method.id });
},
});

if (status === 'unsupported') return <WlasnaListaPunktow method={method} />;
if (status === 'error') return <p role="alert">Nie udało się wczytać mapy: {error?.message}</p>;

return <div {...mountProps} className="h-96 w-full" />;
}

Punkt trafia do koszyka w polu address.pickupPoint:

PickupPointInputinputPełna referencja →

Pickup point (parcel locker / collection point) for a cart shipping address

PoleTypOpis
addressStringPoint address as a single line
nameStringHuman-readable point name
pointIdString!Point identifier within the provider network
providerString!Courier network code (e.g. inpost, orlen, dpd)
Kolejność ma znaczenie

selectShippingMethod dla metody z punktem odbioru zadziała dopiero wtedy, gdy adres koszyka niesie już pickupPoint — inaczej dostaniesz błąd PICKUP_POINT_REQUIRED. Jeśli wybierasz metodę wcześniej (np. zaraz po kliknięciu w kafel), odłóż to wywołanie do momentu wyboru punktu.

Gdy montujesz mapę samodzielnie

Wszystko, czego potrzebuje mapa, jest w pickupConfig.widget:

ShippingPickupWidgetobjectPełna referencja →
PoleTypOpis
attributes[ShippingWidgetAttribute!]!Attributes to place on the mount element. Empty when the widget needs none.
cssUrlStringStylesheet the widget needs, when the carrier ships one separately from the script. Skipping it renders the map unstyled — do not derive this address yourself.
scriptSrcStringAddress of the widget script — load it EXACTLY as given. It already carries whatever the carrier requires in the query string, including the public map token for carriers that authenticate the script request itself (Orlen answers 403 without it). Appending anything of your own breaks it. Null when the carrier ships no script.
scriptUrlStringBare script address, without the query parameters the carrier requires. Kept for storefronts written before `scriptSrc` existed.
scriptVersionStringThe carrier loader's version, when it has one. Informational: it is already part of `scriptSrc`, so appending it yourself produces an address the carrier rejects.
typeString!Which widget this is, e.g. `inpost_geowidget` or `orlen_map`. Use it to pick the right mounting code instead of branching on the carrier name.

Trzy rzeczy, które łatwo przeoczyć, a każda kończy się niedziałającą mapą:

  • cssUrl to osobny plik. Bez niego mapa wyrenderuje się bez stylów. Nie wyprowadzaj tego adresu z adresu skryptu — przewoźnik może serwować oba z różnych hostów.
  • scriptVersion, gdy jest podany, dokleja się do adresu skryptu (?v=…). Loader części przewoźników bez tego nie wystartuje.
  • Atrybuty ustaw PRZED wstawieniem elementu do dokumentu. Widżet czyta swoją konfigurację raz, przy podpięciu; token ustawiony później jest ignorowany. Objaw jest mylący — pierwsze otwarcie mapy działa, drugie pokazuje pustą mapę.

Rozróżniaj mapy po widget.type, nie po nazwie przewoźnika — dzięki temu kolejny przewoźnik korzystający z tej samej mapy nie wymaga zmian w Twoim kodzie.

Gdy selectionMode === 'SEARCH', widgetToken i scriptUrlnull — nie ładujesz widżetu. Wyszukaj punkty po stronie serwera (po mieście / kodzie pocztowym), wyrenderuj własną listę i — po wyborze — ustaw adres z pickupPoint tak samo jak wyżej.

Publiczny endpoint REST zwraca punkty dla wybranego providera:

GET /storefront/v1/{shopSlug}/shipping/points?provider=inpost&city=Kraków

Każdy zwrócony punkt niesie pole paymentAvailable (czy obsługuje pobranie — patrz niżej). Dla koszyka z płatnością za pobraniem dołóż codOnly=true, aby dostać wyłącznie punkty inkasujące gotówkę:

GET /storefront/v1/{shopSlug}/shipping/points?provider=inpost&codOnly=true

Pobranie (COD) do paczkomatu

Nie każdy paczkomat obsługuje płatność za pobraniem. Wybrany punkt niesie tę informację w polu paymentAvailable:

PickupPointobjectPełna referencja →

A pickup point (parcel locker or staffed collection point) attached to a delivery address. The buyer picks it in the carrier widget; it is persisted on the cart shipping address and carried through to the order.

PoleTypOpis
addressStringPoint address as a single human-readable line — show alongside the name on the confirmation page.
nameStringDisplay name of the point as shown in the carrier widget.
paymentAvailableBooleanWhether this pickup point accepts cash on delivery (COD). Read from the cart snapshot, captured when the point was selected. Null when unknown — checkout only blocks a COD order when this is explicitly false.
pointIdString!Identifier of the point within the provider network (e.g. an InPost parcel locker code such as `KRA010`). Pass back to `PickupPointInput.pointId` when attaching the same point to another address.
providerString!Carrier network code that owns the point (e.g. `inpost`, `orlen`, `dpd`).

Interpretacja paymentAvailable:

  • true — punkt przyjmuje pobranie.
  • false — punkt nie przyjmuje pobrania.
  • null — brak informacji (nieznane). Finalizacja blokuje zamówienie za pobraniem tylko przy jawnym false (fail-open — nieznana wartość nigdy nie blokuje kupującego).

Wartość jest zatrzaskiwana w chwili wyboru punktu (przy setShippingAddress) i utrwalana na koszyku, więc cartComplete weryfikuje ją bez dodatkowego zapytania do kuriera.

Tryb geowidgetu wybiera storefront. Mapa domyślnie pokazuje wszystkie paczkomaty (config="parcelCollect"). Jeśli wiesz, że koszyk pójdzie za pobraniem, zawęź mapę do punktów z inkasem, ustawiając config="parcelCollectPayment" — kupujący zobaczy wtedy tylko paczkomaty obsługujące COD. Backend nie narzuca trybu widżetu ani nie filtruje punktów za Ciebie; spójność gwarantuje bramka cartComplete (niżej) oraz walidacja przy nadaniu przesyłki po stronie sklepu.

// Tryb mapy zależnie od metody płatności wybranej w koszyku.
// Dla pobrania pokaż wyłącznie paczkomaty z inkasem; w przeciwnym razie wszystkie.
const widgetMode = isCodSelected ? 'parcelCollectPayment' : 'parcelCollect';
// przekaż `widgetMode` do atrybutu `config` geowidżetu paczkomatów

W trybie SEARCH odpowiednikiem tego zawężenia jest parametr codOnly=true (wyżej).

Walidacja przy finalizacji

cartSelectShippingMethod dla metody punktu odbioru bez ustawionego punktu zwraca błąd w userErrors z kodem PICKUP_POINT_REQUIRED — zawsze ustaw adres z pickupPoint przed wyborem metody paczkomatowej.

cartComplete dla płatności za pobraniem (CASH_ON_DELIVERY) do punktu, który jawnie nie obsługuje inkasa (paymentAvailable === false), zwraca userErrors[{ code: 'COD_PICKUP_POINT_NOT_SUPPORTED' }]. Obsłuż go w UI — poproś kupującego o wybór innego paczkomatu (z pobraniem) albo o zmianę metody płatności na online. Gdy dobrałeś tryb widżetu (config) lub filtr codOnly do metody płatności, ten błąd nie powinien wystąpić w normalnym flow — traktuj go jako zabezpieczenie brzegowe (np. gdy kupujący zmienił płatność już po wyborze punktu).

3. Płatność (method-centric)

Kupujący wybiera kategorię płatności (methodType: BLIK / CARD / BANK_TRANSFER / WALLET / INSTALLMENT / CASH_ON_DELIVERY) z odpowiedzi availablePaymentMethods. Backend routuje do konkretnego providera wg priorytetów merchanta. Opcjonalny preferredInstrument deep-linkuje do konkretnego instrumentu (kod BLIK, wybrany bank) — patrz Pre-selekcja instrumentów płatności.

CartSelectPaymentMethodmutationCart Completion Mutations
mutation CartSelectPaymentMethod($input: CartSelectPaymentMethodInput!)

Selects a payment method on the cart by category (`methodType` — BLIK, CARD, BANK_TRANSFER, ...). Optional `preferredProvider` overrides the merchant priority when the buyer explicitly picks a gateway from `PaymentMethod.providersAvailable`. The backend resolves the gateway routing from `MerchantPaymentConfig` at `cartComplete`; the selection persisted here is the category. Errors: `PAYMENT_METHOD_REQUIRED`, `CART_NOT_FOUND`.

4. Finalizacja zamówienia

CartCompletemutationCart Completion Mutations
mutation CartComplete($input: CartCompleteInput!)

Finalizes the cart — creates the `Order`, deducts gift cards, sends order-created confirmation — all atomically. **Idempotent on `idempotencyKey`** (auto-generated from cartId + minute timestamp if caller omits it). Returns `order` field after completion. Note: `paymentUrl` is intentionally NOT in payload — for hosted gateways (online providers) the storefront calls a separate `paymentCreate` mutation after this returns (check `order.canCreatePayment` first). Errors: `EMAIL_REQUIRED`, `SHIPPING_ADDRESS_REQUIRED`, `SHIPPING_METHOD_REQUIRED`, `PAYMENT_METHOD_REQUIRED`, `INSUFFICIENT_STOCK`, `ALREADY_COMPLETED`.

Variables

NameTypeDefaultRequired
$inputCartCompleteInput!Yes
GraphQL operation
mutation CartComplete($input: CartCompleteInput!) {
cartComplete(input: $input) {
order {
...Order
}
userErrors {
...UserError
}
warnings {
...CartWarning
}
}
}
Uses fragments: CartWarning, Order, UserError

cartComplete zwraca Order (zawsze non-null na sukces — bez dodatkowego zapytania). Storefront czyta capability signal order.canCreatePayment:

  • true → flow online: wywołaj paymentCreate, przekieruj na bramkę (ONLINE_REDIRECT) lub osadź widget (ONLINE_EMBEDDED).
  • false → flow offline (pobranie, przelew): pokaż instrukcje płatności, bez przycisku „Zapłać".
PaymentCreatemutationCart Completion Mutations
mutation PaymentCreate($input: PaymentCreateInput!)

Initiates a payment session for an order created by `cartComplete` — call this when `order.canCreatePayment` is `true` (orders with an offline payment method like cash-on-delivery skip this step). The session charges the order's **outstanding balance** (total minus settled captures plus settled refunds), not necessarily the full total — a gift-card-covered part or an earlier partial payment is never charged twice; pending (unfinished) sessions do not reduce the balance. `orderId` is required; `returnUrl` / `cancelUrl` are optional and, when supplied, must point to a verified domain of the shop (open-redirect protected). Branch on the returned `payment.flow`: `ONLINE_REDIRECT` → redirect to `payment.redirectUrl`, `ONLINE_EMBEDDED` → render a widget with `payment.clientSecret`, `INSTANT_DIRECT` → already settled, read `payment.status`. Public, but ownership-checked — an authenticated customer cannot pay for another customer's order. **Idempotent** — calling it again for the same order returns the existing still-valid session **as long as its amount still equals the outstanding balance**; when the balance changed in the meantime (e.g. the shop edited the order items), a fresh session on the current balance is created instead (safe to retry either way). Rate limit: 5 requests/minute. `userErrors[].code`: `ORDER_NOT_FOUND`, `ORDER_ALREADY_PAID`, `ORDER_NOT_PAYABLE`, `PAYMENT_PROVIDER_NOT_CONFIGURED`, `RETURN_URL_INVALID`, `INVALID_ID_FORMAT`, `PAYMENT_FAILED`, `INSTRUMENT_PRESELECTION_FAILED`. `warnings[]` array zawiera `INSTRUMENT_CLEARED_FOR_RETRY` post-auto-clear instrument-specific failure (storefront retry hint: next attempt uses gateway default landing).

Pełen przykład — useCartManager

'use client';
import { useCartManager } from '@doswiftly/storefront-sdk/react';
import { useRouter } from 'next/navigation';

export function CheckoutSubmit() {
const router = useRouter();
const { complete, createPayment, status } = useCartManager();

async function onSubmit() {
// Zakładamy, że adres + metoda wysyłki + metoda płatności są już wybrane
// (setShippingAddress / selectShippingMethod / selectPaymentMethod wyżej w formularzu).
const { order } = await complete({ idempotencyKey: crypto.randomUUID() });
// cookie cart-id jest już wyczyszczone — kolejny addItem tworzy świeży koszyk.

const successUrl = `/checkout/success?token=${order.accessToken}&orderNumber=${order.orderNumber}`;

if (order.canCreatePayment) {
const session = await createPayment({
orderId: order.id,
returnUrl: `${window.location.origin}${successUrl}`,
});
if (session.flow === 'ONLINE_REDIRECT') {
window.location.href = session.redirectUrl!;
return;
}
}
router.push(successUrl);
}

return (
<button onClick={onSubmit} disabled={status.type === 'loading'}>
{status.type === 'loading' ? `Pracuję — ${status.operation}` : 'Złóż zamówienie'}
</button>
);
}

order.accessToken to opaque token do widoku zamówienia bez konta — przekazujesz go na stronę potwierdzenia, gdzie OrderByToken zwraca podsumowanie.

Następne kroki