Odtworzenie zaznaczeń kasy po odświeżeniu
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
Kasę, która pamięta wybory kupującego: po odświeżeniu strony, powrocie z innej karty albo wejściu w link „wróć do kasy" z e-maila podsumowanie dostawy i płatności wygląda dokładnie tak, jak przed wyjściem — zamiast pustej kasy zaczynającej od zera.
Sedno przepisu to jedna zasada:
Koszyk jest jedynym źródłem prawdy o zaznaczeniach. Nie trzymaj kroku kasy ani wybranych opcji w stanie komponentu — po odświeżeniu strony stan lokalny zniknie, a koszyk pamięta wszystko. Interfejs się odtwarza z koszyka, a gdy trzeba, zaznaczenia wracają do interfejsu tymi samymi mutacjami, którymi powstają.
Wymagania
- Skonfigurowany SDK — Konfiguracja Next.js.
- Koszyk utworzony przez
cartCreate— to jego cookie przemieszcza zaznaczenia między odświeżeniami strony. - Krok dostawy i płatności zbudowane z Wybór punktu odbioru w kasie lub Kasa.
Krok 1 — Odczytaj zaznaczenia z koszyka
- Client Component
- Raw (dowolny framework)
'use client';
import { useCart } from '@/lib/graphql/hooks';
const { data, isLoading, error } = useCart({ id: '…' });
// Działa w dowolnym frameworku (Vue, Svelte, vanilla JS, Node, Edge)
const QUERY = `query Cart($id: ID!) {
cart(id: $id) {
...Cart
}
}
fragment Cart on Cart {
id
checkoutUrl
totalQuantity
cost {
...CartCost
}
lines(first: 100) {
edges {
cursor
node {
... on CartLine {
...CartLine
}
}
}
nodes {
... on CartLine {
...CartLine
}
}
pageInfo {
...PageInfo
}
totalCount
}
buyerIdentity {
...CartBuyerIdentity
}
discountCodes {
...CartDiscountCode
}
discountAllocations {
...CartDiscountAllocation
}
note
attributes {
key
value
}
email
phone
shippingAddress {
...MailingAddress
}
billingAddress {
...MailingAddress
}
selectedShippingMethod {
...CartShippingMethod
}
selectedPaymentMethod {
...CartSelectedPaymentMethod
}
selectedPaymentProvider
selectedPaymentInstrument
appliedGiftCards {
...CartAppliedGiftCard
}
requiresShipping
createdAt
updatedAt
status
completedOrder {
id
orderNumber
accessToken
status
paymentStatus
fulfillmentStatus
}
}
fragment CartAppliedGiftCard on CartAppliedGiftCard {
id
maskedCode
lastCharacters
appliedAmount {
...Money
}
remainingBalance {
...Money
}
}
fragment Money on Money {
amount
currencyCode
}
fragment CartBuyerIdentity on CartBuyerIdentity {
email
phone
countryCode
}
fragment CartCost on CartCost {
total {
...Money
}
subtotal {
...Money
}
totalTax {
...Money
}
feeTotal {
...Money
}
feeAllocations {
label
amount {
...Money
}
}
pricesIncludeTax
totalDuty {
...Money
}
checkoutCharge {
...Money
}
totalDiscount {
...Money
}
totalShipping {
...Money
}
}
fragment CartDiscountAllocation on CartDiscountAllocation {
discountCode
amount {
...Money
}
}
fragment CartDiscountCode on CartDiscountCode {
code
isApplicable
}
fragment CartLine on CartLine {
id
quantity
variant {
...ProductVariant
}
cost {
...CartLineCost
}
discountAllocations {
discountCode
amount {
...Money
}
}
attributes {
key
value
}
attributeSelections {
...AttributeSelection
}
productId
productTitle
productHandle
productType
requiresShipping
giftCardRecipient {
recipientEmail
recipientName
message
}
}
fragment AttributeSelection on AttributeSelection {
attributeDefinitionId
attributeName
type
fillingMode
billingMode
optionId
optionLabel
optionIds
textValue
surchargeAmount
surchargeType
taxClassId
linkedVariantId
}
fragment CartLineCost on CartLineCost {
pricePerUnit {
...Money
}
subtotal {
...Money
}
total {
...Money
}
compareAtPricePerUnit {
...Money
}
}
fragment ProductVariant on ProductVariant {
id
title
sku
price {
...Money
}
compareAtPrice {
...Money
}
isAvailable
availableStock
image {
...ImageThumbnail
}
selectedOptions {
...SelectedOption
}
barcode
weight {
value
unit
}
sortOrder
}
fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}
fragment SelectedOption on SelectedOption {
name
value
}
fragment CartSelectedPaymentMethod on PaymentMethod {
id
name
provider
type
icon {
...ImageThumbnail
}
description
isDefault
supportedCurrencies
position
}
fragment CartShippingMethod on CartShippingMethod {
handle
title
price {
...Money
}
}
fragment MailingAddress on MailingAddress {
id
streetLine1
streetLine2
buildingNumber
flatNumber
city
company
country
countryCode
firstName
lastName
name
phone
state
stateCode
postalCode
isDefault
taxId
vatNumber
regon
pickupPoint {
...PickupPoint
}
}
fragment PickupPoint on PickupPoint {
provider
pointId
name
address
paymentAvailable
}
fragment PageInfo on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}`;
const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { id: '…' }, }),
});
const { data } = await res.json();
Trzy pola odpowiadają za odtworzenie:
selectedShippingMethod— wybrana dostawa: tytuł („DPD Standard, 2 dni"), cena ihandle;selectedPaymentMethod— wybrany typ płatności (CARD, BLIK, BANK_TRANSFER…);selectedPaymentProvideriselectedPaymentInstrument— dostawca i konkretna pozycja wewnątrz metody (to one odróżniają kafelek „mBank" od kafelka „Santander" w ramach tej samej metody).
Z tej trójki wynika też aktualny krok kasy: brak zaznaczonej dostawy = krok dostawy, brak zaznaczonej płatności = krok płatności, oba zaznaczone = podsumowanie. Liczenie kroku z koszyka, a nie ze stanu, sprawia że kasa otwiera się zawsze w tym miejscu, w którym kupujący skończył.
Krok 2 — Złóż kasę odczytującą zaznaczenia
'use client';
import { useCallback, useState } from 'react';
import { useCartManager, useFormatPrice } from '@doswiftly/storefront-sdk/react';
import { CartWarningCode } from '@doswiftly/storefront-sdk';
import type { Cart } from '@doswiftly/storefront-sdk';
// Koszyk jest jedynym źródłem prawdy o tym, gdzie kupujący jest w kasie:
// wybór dostawy wisi na `cart.selectedShippingMethod`, wybór płatności na
// `cart.selectedPaymentMethod`. Nie trzymaj tego w stanie komponentu — po
// odświeżeniu strony stan lokalny zniknie, a koszyk pamięta wszystko.
export function checkoutStepFromCart(cart: Cart): 'shipping' | 'payment' | 'summary' {
if (cart.selectedShippingMethod == null) return 'shipping';
if (cart.selectedPaymentMethod == null) return 'payment';
return 'summary';
}
// Zestawienie zaznaczonych opcji — po odświeżeniu strony kasa odczytuje je z
// koszyka i pokazuje dokładnie to, co kupujący widział przed wyjściem.
export function SelectionSummary({ cart }: { cart: Cart }) {
const formatPrice = useFormatPrice();
return (
<dl>
<dt>Dostawa</dt>
<dd>
{cart.selectedShippingMethod == null ? (
'niewybrana'
) : (
<>
{cart.selectedShippingMethod.title} —{' '}
{formatPrice(cart.selectedShippingMethod.price)}
</>
)}
</dd>
<dt>Płatność</dt>
<dd>
{cart.selectedPaymentMethod == null ? (
'niewybrana'
) : (
cart.selectedPaymentMethod.name
)}
</dd>
</dl>
);
}
// Odtworzenie zaznaczeń po tym, jak stan interfejsu się zgubił (przeładowanie
// strony, powrót z innej karty, nowo zamontowany komponent kasy). Koszyk
// pamięta wybory, więc odtwarzanie to po prostu TE SAME mutacje co zwykły
// wybór — z wartościami zapisanymi na koszyku, nie z lokalnego stanu.
export function useCheckoutSelectionRestore(cart: Cart) {
const { selectShippingMethod, selectPaymentMethod } = useCartManager();
const [warning, setWarning] = useState<string | null>(null);
const restore = useCallback(async () => {
setWarning(null);
const shipping = cart.selectedShippingMethod;
if (shipping != null) {
// `handle` odtworzonej dostawy odpowiada `id` metody z listy dostępnych.
await selectShippingMethod({ shippingMethodId: shipping.handle });
}
const payment = cart.selectedPaymentMethod;
if (payment != null) {
const outcome = await selectPaymentMethod({
methodType: payment.type,
// Dostawca i konkretna pozycja przychodzą z powrotem jako `preferred*`,
// żeby po odtworzeniu zaznaczony był dokładnie ten sam kafelek — sam
// typ metody nie odróżnia np. dwóch przelewów u różnych dostawców.
preferredProvider: cart.selectedPaymentProvider ?? undefined,
preferredInstrument: cart.selectedPaymentInstrument ?? undefined,
});
// Ostrzeżenie zamiast błędu: odtworzenie się udało, ale zapisany wybór
// przestał być ważny (np. metoda została wyłączona, odkąd kupujący był
// na stronie). Rozpoznaj go po kodzie, nie po treści — komunikat jest
// przetłumaczony, ale kod jest stały. Kupujący wybiera formę na nowo;
// nie przeskakuj tego kroku za niego.
if (outcome.warnings.some((w) => w.code === CartWarningCode.PaymentSelectionStale)) {
setWarning('Zapisana forma płatności jest już niedostępna — wybierz inną.');
}
}
}, [cart, selectShippingMethod, selectPaymentMethod]);
return { restore, warning };
}
Trzy rzeczy warte uwagi w tym kodzie:
- Krok kasy liczony jest z koszyka (
checkoutStepFromCart) — to funkcja od stanu serwera, nie od tego, co komponent zdążył zapamiętać. - Odtworzenie używa tych samych mutacji co zwykły wybór. Nie ma osobnej ścieżki „przywróć" — wartości pochodzą z koszyka, więc dokładnie jeden kod obsługuje wybór i jego odtworzenie.
- Dostawca i pozycja wracają jako
preferred*, żeby po odświeżeniu zaznaczony był dokładnie ten sam kafelek. Sam typ metody nie odróżnia dwóch przelewów u różnych dostawców.
Krok 3 — Gdy zapisany wybór przestał być ważny
Odtworzenie może spotkać się z odmową pośrednią: mutacja przechodzi, ale w odpowiedzi przychodzi ostrzeżenie z kodem PAYMENT_SELECTION_STALE — zapisana forma płatności jest już niedostępna (sklep ją wyłączył, zmieniła się waluta, wygasła sesja bramki). Kod jest stały, rozpoznawaj go po nim, nie po treści komunikatu.
W takim wypadku wróć do kroku płatności i pozostaw wybór kupującemu. Nie wybieraj za niego pierwszej dostępnej metody: koszt ma w tym momencie znaczenie realne (np. dopłata za pobranie — patrz Koszt formy płatności na kafelku).
Do jawnego wycofania zaznaczenia (kupujący cofnął wybór w interfejsie) służy osobna mutacja:
- 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 = `mutation CartClearPaymentSelection($input: CartClearPaymentSelectionInput!) {
cartClearPaymentSelection(input: $input) {
cart {
...Cart
}
userErrors {
...UserError
}
warnings {
...CartWarning
}
}
}
fragment Cart on Cart {
id
checkoutUrl
totalQuantity
cost {
...CartCost
}
lines(first: 100) {
edges {
cursor
node {
... on CartLine {
...CartLine
}
}
}
nodes {
... on CartLine {
...CartLine
}
}
pageInfo {
...PageInfo
}
totalCount
}
buyerIdentity {
...CartBuyerIdentity
}
discountCodes {
...CartDiscountCode
}
discountAllocations {
...CartDiscountAllocation
}
note
attributes {
key
value
}
email
phone
shippingAddress {
...MailingAddress
}
billingAddress {
...MailingAddress
}
selectedShippingMethod {
...CartShippingMethod
}
selectedPaymentMethod {
...CartSelectedPaymentMethod
}
selectedPaymentProvider
selectedPaymentInstrument
appliedGiftCards {
...CartAppliedGiftCard
}
requiresShipping
createdAt
updatedAt
status
completedOrder {
id
orderNumber
accessToken
status
paymentStatus
fulfillmentStatus
}
}
fragment CartAppliedGiftCard on CartAppliedGiftCard {
id
maskedCode
lastCharacters
appliedAmount {
...Money
}
remainingBalance {
...Money
}
}
fragment Money on Money {
amount
currencyCode
}
fragment CartBuyerIdentity on CartBuyerIdentity {
email
phone
countryCode
}
fragment CartCost on CartCost {
total {
...Money
}
subtotal {
...Money
}
totalTax {
...Money
}
feeTotal {
...Money
}
feeAllocations {
label
amount {
...Money
}
}
pricesIncludeTax
totalDuty {
...Money
}
checkoutCharge {
...Money
}
totalDiscount {
...Money
}
totalShipping {
...Money
}
}
fragment CartDiscountAllocation on CartDiscountAllocation {
discountCode
amount {
...Money
}
}
fragment CartDiscountCode on CartDiscountCode {
code
isApplicable
}
fragment CartLine on CartLine {
id
quantity
variant {
...ProductVariant
}
cost {
...CartLineCost
}
discountAllocations {
discountCode
amount {
...Money
}
}
attributes {
key
value
}
attributeSelections {
...AttributeSelection
}
productId
productTitle
productHandle
productType
requiresShipping
giftCardRecipient {
recipientEmail
recipientName
message
}
}
fragment AttributeSelection on AttributeSelection {
attributeDefinitionId
attributeName
type
fillingMode
billingMode
optionId
optionLabel
optionIds
textValue
surchargeAmount
surchargeType
taxClassId
linkedVariantId
}
fragment CartLineCost on CartLineCost {
pricePerUnit {
...Money
}
subtotal {
...Money
}
total {
...Money
}
compareAtPricePerUnit {
...Money
}
}
fragment ProductVariant on ProductVariant {
id
title
sku
price {
...Money
}
compareAtPrice {
...Money
}
isAvailable
availableStock
image {
...ImageThumbnail
}
selectedOptions {
...SelectedOption
}
barcode
weight {
value
unit
}
sortOrder
}
fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}
fragment SelectedOption on SelectedOption {
name
value
}
fragment CartSelectedPaymentMethod on PaymentMethod {
id
name
provider
type
icon {
...ImageThumbnail
}
description
isDefault
supportedCurrencies
position
}
fragment CartShippingMethod on CartShippingMethod {
handle
title
price {
...Money
}
}
fragment MailingAddress on MailingAddress {
id
streetLine1
streetLine2
buildingNumber
flatNumber
city
company
country
countryCode
firstName
lastName
name
phone
state
stateCode
postalCode
isDefault
taxId
vatNumber
regon
pickupPoint {
...PickupPoint
}
}
fragment PickupPoint on PickupPoint {
provider
pointId
name
address
paymentAvailable
}
fragment PageInfo on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
fragment CartWarning on CartWarning {
message
code
target
}
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: { input: /* … */ }, }),
});
const { data } = await res.json();
Typy
The shipping method currently selected on a cart, with its display label and cost.
| Pole | Typ | Opis |
|---|---|---|
handle | ID! | Handle of the selected shipping method — matches the `id` of an `AvailableShippingMethod`. |
price | Money! | Cost of the selected shipping method. |
title | String! | Display title for the checkout summary (e.g. "DPD Standard, 2 days"). |
Cart warning (non-fatal advisory)
| Pole | Typ | Opis |
|---|---|---|
code | CartWarningCode! | Stable machine-readable warning code. Branch on this for UI behaviour — never on `message` (translated, locale-dependent). |
message | String! | Human-readable warning message — informational, the mutation itself succeeded. |
target | String! | What the warning is about — a cart line ID when the warning refers to a specific item, or a field path otherwise. |
A payment method offered to the buyer at checkout — what to render in the payment picker and pass to `cartSelectPaymentMethod`.
| Pole | Typ | Opis |
|---|---|---|
acknowledgements | [PaymentAcknowledgement!]! | Consents the buyer can affirm before paying with this method (e.g. the Przelewy24 regulation declaration). Render each as a checkbox using `statement` + `documents`, then echo accepted `code`s back in `PaymentCreateInput.acknowledgements`. Empty when the method carries no acknowledgements. |
available | Boolean! | True when the buyer can actually pick this method right now. False when the resolving gateway is temporarily unavailable (incident/maintenance) or reported the method as disabled. Storefront UI should gray-out the tile when false instead of hiding it — gives merchants observability into routing failures. |
description | String | Optional buyer-facing description shown under the name (e.g. "Pay with your bank app"). |
fee | PaymentMethodFee | Surcharge for picking this method, ready to render on its tile (e.g. "Cash on delivery +5 zł"). Present only when the whole method maps to one fee identity: cash on delivery, or a method whose every instrument shares it. The amount follows the provider that handles the payment by default (the preferred one) — when several providers back the method with different fee setups, per-provider amounts live on `instruments[].fee`. Null when identities differ, when no fee applies, or on the shop-level query — only `Cart.availablePaymentMethods` carries amounts. |
icon | Image | Icon image for the method tile in the payment picker. When the merchant uploaded custom artwork, `url` is absolute and ready to render. When they did not, the platform emits a RELATIVE fallback path following the `/icons/payment/{provider}.svg` convention (`provider` = lowercase provider code, e.g. `payu`, `przelewy24`, `bank_transfer`) — such a path is NOT served by the API: either ship matching files with the storefront or ignore relative URLs and derive artwork from `type`. Never feed a relative `url` straight into an img tag. |
id | ID! | Stable ID of the payment method. Pass to `cartSelectPaymentMethod` to select it. |
instruments | [PaymentInstrument!] | Concrete instruments exposed by gateway providers within this method (BLIK code, branded banks, wallets, card brands). Null when no provider exposes granular data for this method. Empty array when a gateway exposes them but all instruments are disabled or removed by post-filtering (cross-provider leak prevention). Render the list and pass `code` as `preferredInstrument` (together with `preferredProvider`) in `cartSelectPaymentMethod` to deep-link the gateway to this screen. Key list items and selection state by the (provider, code) PAIR — `code` alone is not unique in this list (two providers can expose the same code for one method). An instrument with no `brandImage` whose `displayName` merely repeats the method category or its own code adds nothing over the method tile — consider hiding such entries and rendering the picker only when two or more presentable instruments remain. |
isDefault | Boolean! | True when the merchant has marked this method as the default. Pre-select it in the picker. |
name | String! | Display name configured by the merchant (e.g. "BLIK", "Credit card", "Cash on delivery"). |
position | Float! | Merchant-configured display position — lower values come first in the picker. |
preferredProvider | PaymentProvider | Preferred provider (UPPERCASE enum) that the backend will route to when the buyer picks this method type and does not specify `preferredProvider`. Populated only when at least one provider supports the type. |
provider | PaymentProvider! | Provider (e.g. `PAYU`, `STRIPE`, `PRZELEWY24`, `CASH_ON_DELIVERY`). Identifies the integration behind the method; do not branch UI on it — use `type` instead. |
providersAvailable | [PaymentProvider!] | Providers (UPPERCASE enum: `PAYU`, `PRZELEWY24`, ...) that can fulfil this method type for the current shop, ordered by merchant priority. Pre-select `preferredProvider`; expose the rest only when the buyer wants to choose explicitly. Single-element array when only one provider supports the type. |
supportedCurrencies | [String!] | ISO 4217 currency codes the method accepts. Null when the method accepts the shop currency without restriction. |
type | PaymentMethodType! | Category of the method (CARD, BLIK, BANK_TRANSFER, INSTALLMENT, WALLET, CASH_ON_DELIVERY, OTHER). Drives iconography and copy. |
unavailableReason | PaymentMethodUnavailableReason | When `available` is false, this enum carries the diagnostic reason (GATEWAY_DOWN, GATEWAY_DISABLED, NO_INSTRUMENTS, CREDENTIALS_INVALID). Null when `available` is true. UI can render context-aware copy ("PayU is temporarily down" vs "Method not configured"). |
Powiązane
- Kasa — pełny przebieg kroku dostawy i płatności.
- Instrumenty płatności — pozycje wewnątrz metody płatności (
preferredInstrument,preferredProvider) i kiedy ich używać. - Wybór punktu odbioru w kasie — krok dostawy, którego zaznaczenie odtwarza ten przepis.
- Koszt formy płatności na kafelku — kwoty dopłat, o których mowa, gdy kupujący wybiera formę na nowo.