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.
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 Methodsquery 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
| Name | Type | Default | Required |
|---|---|---|---|
$cartId | ID! | — | Yes |
$address | ShippingAddressInput! | — | 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 Methodsquery 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
}
}
AvailablePaymentMethods2. Adres i metoda wysyłki
CartSetShippingAddressmutationCart Completion Mutationsmutation 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 Mutationsmutation 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.
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:
Delivery destination type of a shipping method — signals whether the storefront must collect a pickup point.
| Wartość | Opis |
|---|---|
HOME | Courier delivery to the street address — no pickup point required. |
LOCKER | Automated parcel locker — the buyer collects from a 24/7 self-service machine. |
PICKUP_POINT | Staffed 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:
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`).
| Pole | Typ | Opis |
|---|---|---|
provider | String! | Carrier code this config belongs to (e.g. "inpost"). Matches `carrier.serviceCode`'s provider. |
scriptUrl | String | CDN URL of the carrier widget script to load before rendering the map. Null for SEARCH mode. |
selectionMode | PickupSelectionMode! | How to let the buyer pick a point — WIDGET (carrier map) or SEARCH (server-side point lookup). |
widget | ShippingPickupWidget | Everything 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. |
widgetToken | String | PUBLIC 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:
How the buyer selects a pickup point for a shipping method that requires one.
| Wartość | Opis |
|---|---|
SEARCH | No browser widget — query points server-side (by city / postal code) and render your own list. `widgetToken` / `scriptUrl` are null for this mode. |
WIDGET | Render 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:
Pickup point (parcel locker / collection point) for a cart shipping address
| Pole | Typ | Opis |
|---|---|---|
address | String | Point address as a single line |
name | String | Human-readable point name |
pointId | String! | Point identifier within the provider network |
provider | String! | Courier network code (e.g. inpost, orlen, dpd) |
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:
| Pole | Typ | Opis |
|---|---|---|
attributes | [ShippingWidgetAttribute!]! | Attributes to place on the mount element. Empty when the widget needs none. |
cssUrl | String | Stylesheet the widget needs, when the carrier ships one separately from the script. Skipping it renders the map unstyled — do not derive this address yourself. |
scriptSrc | String | Address 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. |
scriptUrl | String | Bare script address, without the query parameters the carrier requires. Kept for storefronts written before `scriptSrc` existed. |
scriptVersion | String | The 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. |
type | String! | 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ą:
cssUrlto 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.
Tryb SEARCH
Gdy selectionMode === 'SEARCH', widgetToken i scriptUrl są null — 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:
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.
| Pole | Typ | Opis |
|---|---|---|
address | String | Point address as a single human-readable line — show alongside the name on the confirmation page. |
name | String | Display name of the point as shown in the carrier widget. |
paymentAvailable | Boolean | Whether 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. |
pointId | String! | 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. |
provider | String! | 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 jawnymfalse(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 Mutationsmutation 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 Mutationsmutation 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
| Name | Type | Default | Required |
|---|---|---|---|
$input | CartCompleteInput! | — | Yes |
GraphQL operation
mutation CartComplete($input: CartCompleteInput!) {
cartComplete(input: $input) {
order {
...Order
}
userErrors {
...UserError
}
warnings {
...CartWarning
}
}
}
cartComplete zwraca Order (zawsze non-null na sukces — bez dodatkowego zapytania). Storefront czyta capability signal order.canCreatePayment:
true→ flow online: wywołajpaymentCreate, 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 Mutationsmutation 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
- Koszyk — recovery wygasłego koszyka, statusy, kody błędów, pełen diagram lifecycle
- Zamówienia — podsumowanie zamówienia po checkout (gość + zalogowany)
- Pre-selekcja instrumentów płatności — deep-link do konkretnego instrumentu na bramce