Wybór punktu odbioru w kasie
UI (komponenty z hookami i JSX) jest specyficzne dla React / Next.js. W innym frameworku weź zapytanie z zakładki Raw i napisz własny widok.
Co zbudujesz
Krok dostawy obsługujący oba sposoby wskazania punktu odbioru: mapę przewoźnika (paczkomaty, punkty na stacjach) oraz wyszukiwarkę punktów dla metod, które mapy nie mają. Który sposób zobaczy kupujący, decyduje API — storefront nie musi wiedzieć, z którym przewoźnikiem ma do czynienia.
Sedno przepisu to jedno pole:
method.pickupConfig?.selectionMode // 'WIDGET' → mapa, 'SEARCH' → własna lista, brak pola → dostawa pod adres
Rozróżniaj po selectionMode i widget.type, nigdy po nazwie przewoźnika. Ten sam sklep może jutro włączyć kolejnego przewoźnika korzystającego z tej samej mapy — kod oparty o nazwy wymagałby wtedy zmiany, kod oparty o kontrakt nie.
Metody wymagającej punktu nie da się wybrać, zanim adres koszyka nie niesie punktu — API odrzuci wywołanie z kodem PICKUP_POINT_REQUIRED. Dlatego w przepisie najpierw zapisujemy punkt na adresie, a dopiero potem wybieramy metodę. Dla metod dostarczanych pod adres kolejność nie ma znaczenia i metodę wybieramy od razu po kliknięciu.
Punkt musi też należeć do przewoźnika wybranej metody — punkt z innej sieci kończy się kodem PICKUP_POINT_PROVIDER_MISMATCH. W praktyce zdarza się to wtedy, gdy kupujący zmienia metodę po wybraniu punktu: wyczyść wtedy wcześniejszy wybór albo poproś o wskazanie punktu na nowo.
Wymagania
- Skonfigurowany SDK i provider — Konfiguracja Next.js.
- Koszyk z adresem dostawy — Koszyk.
- Sklep z włączoną metodą dostawy do punktu. Mapa pojawia się wtedy, gdy sklep uzupełnił publiczny token mapy danego przewoźnika; bez tokenu ta sama metoda działa dalej, tylko przez wyszukiwarkę punktów.
Krok 1 — Pobierz metody dostawy dla adresu
- Raw (dowolny framework)
Brak gotowego helpera SDK dla tej operacji — użyj raw operation (działa w każdym frameworku).
// Działa w dowolnym frameworku (Vue, Svelte, vanilla JS, Node, Edge)
const QUERY = `query CartAvailableShippingMethods($cartId: ID!, $address: ShippingAddressInput!) {
cart(id: $cartId) {
id
requiresShipping
availableShippingMethods(address: $address) {
methods {
...AvailableShippingMethod
}
freeShippingProgress {
...FreeShippingProgress
}
userErrors {
...UserError
}
}
}
}
fragment AvailableShippingMethod on AvailableShippingMethod {
id
name
description
deliveryType
pickupConfig {
provider
selectionMode
widgetToken
scriptUrl
widget {
type
scriptSrc
cssUrl
attributes {
name
value
}
scriptUrl
scriptVersion
}
}
unavailableReason
carrier {
...ShippingCarrier
}
price {
...Money
}
isFree
estimatedDelivery {
...DeliveryEstimate
}
freeShippingProgress {
...FreeShippingProgress
}
sortOrder
}
fragment DeliveryEstimate on DeliveryEstimate {
minDays
maxDays
description
}
fragment FreeShippingProgress on FreeShippingProgress {
qualifies
currentAmount {
...Money
}
threshold {
...Money
}
remaining {
...Money
}
progressPercent
message
}
fragment Money on Money {
amount
currencyCode
}
fragment ShippingCarrier on ShippingCarrier {
id
name
logo {
...ImageThumbnail
}
serviceCode
}
fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}
fragment UserError on UserError {
message
code
field
}`;
const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { cartId: '…', address: /* … */ }, }),
});
const { data } = await res.json();
Każda metoda niesie deliveryType (dostawa pod adres kontra punkt odbioru) oraz — dla metod z punktem — pickupConfig. Gdy lista wraca pusta, powód jest w userErrors i jest już przetłumaczony; pokaż go bez zmian.
unavailableReasonLista może zawierać metodę, której kupujący nie domknie: jest skonfigurowana przez sprzedawcę i wyceniona, ale nie ma dla niej żadnego sposobu wskazania punktu odbioru (przewoźnik nie jest podłączony albo metodę dodano ręcznie). Taką metodę API oznacza polem unavailableReason; wybór kończyłby się kodem PICKUP_POINT_REQUIRED.
Sprawdzaj to pole przed wyrenderowaniem metody: null znaczy „kupujący może ją wybrać do końca". Wartość znaczy „ukryj albo pokaż jako niedostępną" — nie musisz wiedzieć, jakie wartości może przyjąć, żeby zrobić to poprawnie, więc kolejne powody dopisane w przyszłości niczego u Ciebie nie zepsują.
Krok 2 — Złóż krok dostawy
'use client';
import { useState } from 'react';
import {
useCartManager,
useAvailableShippingMethods,
usePickupPointWidget,
usePickupPointSearch,
} from '@doswiftly/storefront-sdk/react';
import type { AvailableShippingMethod, CartAddressInput } from '@doswiftly/storefront-sdk';
type Method = AvailableShippingMethod;
// Zapisuje wybrany punkt i dopiero POTEM wybiera metodę. Kolejność nie jest kwestią
// stylu: dopóki adres koszyka nie niesie punktu, API odrzuca wybór metody z kodem
// PICKUP_POINT_REQUIRED. Ta funkcja jest wspólna dla mapy i listy, więc obie ścieżki
// zapisują punkt tak samo.
function useSavePickupPoint(method: Method, address: CartAddressInput) {
const { setShippingAddress, selectShippingMethod } = useCartManager();
return async (point: { pointId: string; name?: string; address?: string }) => {
await setShippingAddress({
...address,
pickupPoint: {
provider: method.pickupConfig?.provider ?? '',
pointId: point.pointId,
name: point.name,
address: point.address,
},
});
await selectShippingMethod({ shippingMethodId: method.id });
};
}
// Mapa przewoźnika. Hook ładuje skrypt i arkusz stylów, montuje właściwy element
// i sprząta po odmontowaniu — wystarczy wskazać, gdzie mapa ma się pojawić.
function MapaPunktow({ method, address }: { method: Method; address: CartAddressInput }) {
const savePoint = useSavePickupPoint(method, address);
const { mountProps, status, error } = usePickupPointWidget({
pickupConfig: method.pickupConfig,
onSelect: (point) => void savePoint(point),
});
// Sklep może działać na starszym wydaniu SDK niż mapa, którą zwróciło API.
// Wtedy zamiast pustej ramki pokazujemy listę punktów.
if (status === 'unsupported') return <ListaPunktow method={method} address={address} />;
if (status === 'error') return <p role="alert">Nie udało się wczytać mapy: {error?.message}</p>;
return <div {...mountProps} style={{ height: 400 }} aria-busy={status === 'loading'} />;
}
// Wyszukiwarka punktów — tryb dla metod bez mapy (przewoźnik jej nie ma albo sklep
// nie ma jeszcze publicznego tokenu). Hook sam opóźnia zapytania podczas pisania
// i anuluje te, które przestały być aktualne.
function ListaPunktow({ method, address }: { method: Method; address: CartAddressInput }) {
const [city, setCity] = useState('');
const savePoint = useSavePickupPoint(method, address);
const { points, isLoading } = usePickupPointSearch({
provider: method.pickupConfig?.provider ?? '',
filters: { city },
});
return (
<div>
<label htmlFor="pickup-city">Miasto</label>
<input id="pickup-city" value={city} onChange={(event) => setCity(event.target.value)} />
<ul aria-busy={isLoading}>
{points.map((point) => (
<li key={point.id}>
<button type="button" onClick={() => void savePoint({ pointId: point.id, name: point.name })}>
{point.name} — {point.address.line1}, {point.address.city}
</button>
</li>
))}
</ul>
</div>
);
}
// Krok dostawy: metody dla adresu, a dla metody z punktem odbioru — mapa albo lista.
// `deliveryType` mówi, czy metoda w ogóle wymaga punktu; `pickupConfig.selectionMode`
// rozstrzyga, którym sposobem kupujący go wskaże.
export function KrokDostawy({ cartId, address }: { cartId: string; address: CartAddressInput }) {
const [selected, setSelected] = useState<Method | null>(null);
const { methods, userErrors, isLoading } = useAvailableShippingMethods({ cartId, address });
const { selectShippingMethod } = useCartManager();
if (isLoading) return <p>Sprawdzamy dostępne metody…</p>;
// Metody, których kupujący nie domknie, API oznacza powodem — najczęściej dlatego, że sklep
// nie ma dla nich żadnego sposobu wskazania punktu. Filtrujemy je zamiast pokazywać ofertę,
// która kończy się błędem przy wyborze.
const doWyboru = methods.filter((method) => !method.unavailableReason);
// Powody braku metod przychodzą z API już przetłumaczone — pokaż je bez zmian.
if (!doWyboru.length) return <p>{userErrors[0]?.message ?? 'Brak metod dostawy dla tego adresu.'}</p>;
const wymagaPunktu = selected?.pickupConfig != null;
return (
<div>
{doWyboru.map((method) => (
<button
key={method.id}
type="button"
onClick={() => {
setSelected(method);
// Metodę z punktem wybieramy dopiero po wskazaniu punktu (patrz wyżej).
if (!method.pickupConfig) void selectShippingMethod({ shippingMethodId: method.id });
}}
>
{method.name}
</button>
))}
{wymagaPunktu && selected && selected.pickupConfig?.selectionMode === 'WIDGET' ? (
<MapaPunktow method={selected} address={address} />
) : null}
{wymagaPunktu && selected && selected.pickupConfig?.selectionMode === 'SEARCH' ? (
<ListaPunktow method={selected} address={address} />
) : null}
</div>
);
}
Trzy rzeczy warte uwagi w tym kodzie:
- Zapis punktu jest wspólny dla obu ścieżek. Mapa i lista różnią się sposobem wskazania punktu, ale zapisują go tak samo — dzięki temu kolejność „punkt, potem metoda" jest w jednym miejscu, a nie powielona.
- Stan
unsupportedprowadzi do listy, nie do błędu. API może zwrócić mapę nowszą niż wydanie SDK zainstalowane w sklepie. Wtedy kupujący dostaje wyszukiwarkę zamiast pustej ramki — sprzedaż idzie dalej. - Wyszukiwarka sama opóźnia zapytania podczas pisania i anuluje te nieaktualne, więc wynik dla „Krak" nie zostanie na ekranie, gdy kupujący dopisze „ów".
Gdy mapa nie chce się załadować
Skrypt mapy pobierany jest spod adresu pickupConfig.widget.scriptSrc — ładuj go dokładnie tak, jak przyszedł. Adres niesie już wszystko, czego dany przewoźnik wymaga, łącznie z publicznym tokenem mapy u tych, którzy uwierzytelniają samo pobranie skryptu. Doklejenie własnych parametrów (np. wersji z scriptVersion) psuje adres i kończy się odmową po stronie przewoźnika. Jeśli montujesz mapę samodzielnie, zamiast hookiem SDK, to jest jedyne pole, którego potrzebujesz do pobrania skryptu.
Mapa, która nie jest własnym elementem HTML przewoźnika (tak działa m.in. ORLEN Paczka), powstaje w momencie załadowania skryptu: skrypt przegląda stronę, znajduje elementy z klasą montażową i zamienia je w mapy. Nie obserwuje elementów dodanych później.
Wnioski dla sklepu montującego mapę samodzielnie, bez hooka SDK:
- wstaw element montażu do strony ZANIM pobierzesz skrypt — element dodany po załadowaniu skryptu nie zostanie zamieniony w mapę i kupujący zobaczy pustą, białą ramkę;
- przy ponownym otwarciu poproś przewoźnika o kolejny przegląd — skrypt jest już wtedy na stronie i sam z siebie nic nie zrobi, więc druga i każda następna mapa wymaga wywołania inicjalizacji, którą skrypt wystawia globalnie;
- pusta ramka to awaria, nie stan pośredni — jeśli mapy nie da się zbudować, potraktuj to jak
unsupportedi pokaż wyszukiwarkę punktów; - daj przewoźnikowi gdzie zapisać wybór, inaczej nie pozwoli wybierać — ORLEN renderuje przy punkcie przycisk „Wybierz" tylko wtedy, gdy element montażu wskazuje pola na kod punktu (
data-target="#id") i jego nazwę (data-label="#id"). Bez nich dostajesz mapę do oglądania: punkty widać, wybrać się nie da; - nasłuchuj wyboru w fazie przechwytywania — zdarzenie wyboru nie bąbelkuje i leci na elemencie, który przewoźnik zbudował (mapa osadzona) albo na jego oknie modalnym. Nasłuch na
documentbez przechwytywania nigdy go nie zobaczy, a kupujący będzie klikał „Wybierz" w martwą kasę; - adres punktu składa się z kawałków — przewoźnik nie podaje gotowego adresu, tylko
addressLine,postalCodeicityosobno, anameto sam numer punktu.
Hook usePickupPointWidget robi wszystkie trzy rzeczy za Ciebie: montuje element przed pobraniem skryptu, prosi o ponowny przegląd przy kolejnym otwarciu (pomijając mapy już zbudowane, żeby jedno kliknięcie nie otwierało dwóch okien) i przechodzi w error, gdy mapa nie powstała.
Gdy przewoźnik odmawia mimo poprawnego adresu, rozróżnij dwa przypadki po treści odpowiedzi:
- odmowa dotycząca tokenu — sprzedawca nie uzupełnił publicznego tokenu mapy w panelu albo token wygasł;
- odmowa dotycząca domeny — token jest przypisany do konkretnej domeny sklepu. Przewoźnicy sprawdzają ją zwykle po nagłówku
Referer, więc to samo żądanie z domeny produkcyjnej przechodzi, a zlocalhostnie. Do pracy lokalnej sprzedawca potrzebuje osobnego tokenu testowego od swojego przewoźnika — to nie jest usterka integracji.
Krok 3 — Sprawdź, co trafiło do koszyka
Wybrany punkt wisi na adresie dostawy, więc możesz go odczytać z koszyka i pokazać podsumowanie („Odbiór w: …") bez trzymania własnego stanu:
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`). |
Pole paymentAvailable mówi, czy punkt przyjmuje płatność przy odbiorze. undefined znaczy „przewoźnik tego nie podaje" — to nie to samo co „nie przyjmuje". Dla koszyka z płatnością przy odbiorze przekaż codOnly: true do wyszukiwarki, żeby lista od razu zawierała tylko punkty inkasujące gotówkę.
Typy
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). |
| 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. |
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) |
Powiązane
- Kasa — pełny przebieg kroku dostawy i płatności.
- Koszyk — ustawianie adresu i wybór metody.
- Zamówienia — odczyt wybranego punktu na złożonym zamówieniu.