Przejdź do głównej zawartości

Zamówienia

Kompletny przewodnik po Order GraphQL surface w storefroncie: zalogowany dostęp do własnego zamówienia, guest order summary po checkout bez konta, top-level Order.lineItems z gwarantowaną ochroną przed N+1.

Dostępne operacje

Zapytania

OperationAuthUse case
CustomerOrder($orderId)wymaga sesji (cookie/Bearer)Szczegóły jednego zamówienia dla zalogowanego klienta
OrderByToken($token, $email?)bez authGuest order summary po cartComplete (token z cartComplete.order.accessToken)

Pola Order

Po upgradzie @doswiftly/storefront-operations do najnowszej wersji minor, fragment Order zawiera:

  • accessToken: String! — opaque per-order token używany jako klucz do OrderByToken query. Zwracany w cartComplete.order.accessToken natychmiast po checkout.
  • lineItems(first: Int = 10, after: String): OrderLineItemConnection! — Relay-style connection z line items (top-level, NIE wymaga fetcha shipments).
  • customerNote: String — notatka, którą kupujący zostawił przy składaniu zamówienia (instrukcje dostawy, wiadomość prezentowa itp.); odzwierciedla notatkę ustawioną na koszyku przez CartUpdateNote. null, gdy notatki nie podano.
  • Standardowe: id, orderNumber, status, totals { total, subtotal, totalTax, totalShipping }, paymentStatus, fulfillmentStatus, processedAt, shippingAddress, itemCount, canCreatePayment, paymentMethodType.

Import

// CustomerOrder (zalogowany klient) — client hook
import { useCustomerOrder } from '@/lib/graphql/hooks';

OrderByToken (guest order po tokenie) nie ma template-hooka — to operacja raw-only. Użyj jej przez raw operation (poniżej <RenderingTabs operation="OrderByToken" />) albo metodę SDK CartClient.getOrderByToken(token, email?).

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 OrderByToken($token: String!, $email: String) {
orderByToken(token: $token, email: $email) {
...Order
}
}

fragment Order on Order {
id
orderNumber
accessToken
totals {
total {
...Money
}
subtotal {
...Money
}
totalTax {
...Money
}
totalShipping {
...Money
}
feeTotal {
...Money
}
feeAllocations {
label
amount {
...Money
}
}
pricesIncludeTax
}
status
paymentStatus
fulfillmentStatus
processedAt
confirmedAt
cancelledAt
expiredAt
shippingAddress {
...MailingAddress
}
itemCount
customerNote
discountAllocations {
discountCode
amount {
...Money
}
}
canCreatePayment
paymentMethodType
bankTransferInstructions {
bankName
accountNumber
accountHolder
transferTitle
amount {
...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 Money on Money {
amount
currencyCode
}`;

const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { token: '…', email: '…' }, }),
});
const { data } = await res.json();
informacja

Typed dokumenty codegen pochodzą z @doswiftly/storefront-operations. Po upgradzie SDK i operations uruchom pnpm run codegen.

Guest order summary po checkout

Problem: gość kończy checkout, twój storefront chce pokazać podsumowanie zamówienia od razu — bez zakładania konta i bez czekania na e-mail. customerOrder(orderId) wymaga sesji, więc dla gościa zwraca null.

Rozwiązanie: cartComplete zwraca Order.accessToken (opaque UUID). Storefront persistuje token, redirectuje na stronę summary, używa OrderByToken query.

End-to-end flow

'use client';

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

// `complete()` jest częścią pełnego lifecycle koszyka w useCartManager — patrz [Koszyk](./cart.md).
export function CheckoutButton() {
const router = useRouter();
const { complete, status } = useCartManager();

return (
<button
disabled={status.isLoading}
onClick={async () => {
const { order } = await complete();
if (!order) return;
// Token w URL — server-side render summary
router.push(`/order/summary?token=${order.accessToken}`);
}}
>
Złóż zamówienie
</button>
);
}

Strona summary (Server Component preferowane)

// app/order/summary/page.tsx
import { getStorefrontClient } from '@doswiftly/storefront-sdk/react/server';
import { CartClient } from '@doswiftly/storefront-sdk';
import { OrderSummaryView } from '@/components/order/summary-view';

export default async function OrderSummaryPage({
searchParams,
}: {
searchParams: { token?: string };
}) {
const token = searchParams.token;
if (!token) return <p>Brak tokenu — link nieprawidłowy.</p>;

// OrderByToken nie ma template-hooka — to metoda SDK CartClient (działa server-side)
const order = await new CartClient(getStorefrontClient()).getOrderByToken(token);
if (!order) {
return (
<p>
Nie znaleziono zamówienia. Sprawdź skrzynkę e-mail — wysłaliśmy
potwierdzenie z linkiem do podsumowania.
</p>
);
}

return <OrderSummaryView order={order} />;
}

Token storage best-practices

Token jest opaque secret: kto go ma, ten widzi szczegóły zamówienia (numer, kwota, lista produktów, adres dostawy). Musi być chroniony przed XSS i shared device leakage.

Rekomendowane

StorageUse caseTrade-off
HTTP-only cookie (cart-id-style, sameSite=lax, secure=true)Domyślny dla guest summary, działa SSR + edgeNiewidoczny dla JS = bezpieczny przed XSS; wymaga server-side cookie write w callbacku po cartComplete
sessionStorageStorefront SPA bez SSR, gdy cookie ustawienie niepraktyczneCzyszczony po zamknięciu tab; chroni przed shared device, ale czytelny dla JS (XSS risk)
URL query parameter (?token=...)Tymczasowo w trakcie redirect cartComplete → /order/summaryWycieka do server logs, browser history, Referer header — NIE persistuj URL po render, pobierz token z params i renderuj summary bez retencji w URL

NIGDY

  • localStorage — czytelny dla XSS w storefroncie. Token może być wykradziony przez injected script.
  • Trzymanie tokenu w global state Redux/Zustand bez czyszczenia po render summary — w shared browser kolejny user zobaczy.

Defense-in-depth: opcjonalny email guard

OrderByToken przyjmuje opcjonalny parametr email. Gdy podany, backend porównuje case-insensitive z buyer email zamówienia. Mismatch zwraca null identycznie jak invalid token — atakujący nie odróżni "token poprawny ale email zły" od "token zły".

Kiedy warto użyć email guard

ScenariuszZ email guardBez guard (token-only)
Token wyciekł do server logs / RefererLogi nie zawierają emaila → atakujący ma niekompletne daneAtakujący ma pełny dostęp
Shared device / kiosk z browser historyNastępny user widzi URL bez emaila → mismatch → nullNastępny user widzi order
Storefront ma context emaila z checkout (sessionStorage)Łatwy do podpięcia bez frictionn/a
const cart = new CartClient(getStorefrontClient());

// Wariant z email guard — preferowany jeśli storefront trzyma kontekst checkout
const order = await cart.getOrderByToken(tokenFromUrl, emailFromCheckoutSession);

// Wariant token-only — prostszy, gdy nie masz wygodnego dostępu do email
const order = await cart.getOrderByToken(tokenFromUrl);

Top-level Order.lineItems (eliminuje N+1)

Order.lineItems to Relay-style connection na top-levelu, eliminujący wcześniejszą konieczność fetchowania shipments[].items[] per order.

Order listing — preview produktów per zamówienie

query CustomerOrdersList {
customer {
id
orders(first: 20) {
nodes {
id
orderNumber
totals { total { amount currencyCode } }
lineItems(first: 3) {
nodes {
id
title
quantity
variant { image { url } }
}
}
}
}
}
}

Backend wykonuje stałą liczbę zapytań SQL niezależnie od liczby orderów na listingu (backend pobiera w stałej liczbie zapytań, bez N+1). Wcześniejsze podejście (fetch shipments[].items[] per order) generowało N+1 — 20 orderów × 1 dodatkowy query = 20 SQL hits.

Pola OrderLineItem

PoleTypSemantyka
idID!Stable identifier
titleString!Nazwa produktu z momentu zakupu (snapshot — NIE live Product.name). Preserved nawet po edycji produktu.
quantityInt!Ilość zamówiona
variantProductVariant (nullable)Live wariant produktu — null gdy wariant został usunięty po zakupie. Storefront powinien fallback'ować na title.
originalUnitPriceMoney!Cena jednostkowa z momentu checkout (final, z BUNDLED surcharges)
originalTotalPriceMoney!Całkowita cena linii (unit × quantity)
quantityFulfilledInt!Ile już wysłane (per-item fulfillment)
refundableQuantityInt!Pozostała ilość dostępna do zwrotu (quantity - returnedQuantity, clamp ≥ 0)

Variant deleted handling

{order.lineItems.nodes.map((item) => (
<div key={item.id}>
{item.variant?.image ? (
<img src={item.variant.image.url} alt={item.title} />
) : (
// Fallback gdy wariant został usunięty po zakupie
<DefaultProductImage />
)}
<span>{item.title}</span>
<span>x{item.quantity}</span>
</div>
))}

Bezpieczeństwo

OrderByToken jest chroniony wielowarstwowo:

WarstwaMechanizm
Rate-limit5 zapytań / minuta per IP+shop. Przekroczenie → GraphQL error z extensions.code: THROTTLED. Chroni przed token enumeration.
Email guard (opcjonalny)Patrz sekcja powyżej. Storefront-dev decides per threat model.
Uniform null returnsInvalid token, email mismatch, not found = ten sam null. Atakujący nie wnioskuje "token poprawny ale email zły".
Cache-Control: no-storeResponse nie cache'owany w CDN/proxy/browser. Per-customer data nigdy nie serwowane między userami.
Izolacja między sklepamiDane każdego sklepu są w pełni izolowane — token ze sklepu A nigdy nie zwróci zamówienia sklepu B.

Obsługa rate-limit po stronie storefrontu

import { CartClient, StorefrontError } from '@doswiftly/storefront-sdk';

// getOrderByToken rzuca StorefrontError gdy backend zwróci błąd (np. rate-limit THROTTLED).
// Uwaga: throttling przychodzi jako błąd GraphQL — `err.code` to 'GRAPHQL_ERROR',
// a sygnał THROTTLED znajdziesz w `extensions.code` błędu GraphQL.
async function loadOrderSummary(cart: CartClient, token: string) {
try {
const order = await cart.getOrderByToken(token);
if (!order) return { kind: 'not-found' as const };
return { kind: 'ok' as const, order };
} catch (err) {
if (
err instanceof StorefrontError &&
err.graphqlErrors.some((e) => e.extensions?.code === 'THROTTLED')
) {
// Zbyt wiele prób w krótkim czasie — pokaż komunikat, poproś o sprawdzenie e-maila
return { kind: 'throttled' as const };
}
throw err;
}
}

Migration z 11.x

Brak breaking changes. Po upgradzie @doswiftly/storefront-operations + @doswiftly/storefront-sdk do najnowszej wersji minor:

  • Wszystkie istniejące queries (CustomerOrder, cart mutations, customer auth) działają bez zmian.
  • Nowe pola/queries są opcjonalne — storefront opt-in kiedy potrzebuje.
  • Bezpieczny upgrade: bump wersji w package.json, pnpm install, pnpm run codegen (regenerate typed documents) — storefront działa identycznie. Następnie opcjonalnie dodaj accessToken do swojego fragmentu Order i wprowadź OrderByToken query gdzie potrzebujesz guest summary.

Krok po kroku

  1. Bump wersji w package.json:

    {
    "dependencies": {
    "@doswiftly/storefront-operations": "^X.Y.0",
    "@doswiftly/storefront-sdk": "^X.Y.0"
    }
    }

    (synchronizacja wersji wymagana — paczki są linked).

  2. Regenerate codegen:

    pnpm install
    pnpm run codegen
  3. Opcjonalnie — wpięcie guest summary (przykłady wyżej).

Powiązane