Przejdź do głównej zawartości

Koszyk z rabatami

Framework: React / Next.js
Pobieranie danych jest przenośne — w blokach z przykładami zakładka Raw pokazuje czysty fetch działający w dowolnym frameworku.
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

Podsumowanie koszyka: liczba pozycji, lista zastosowanych kodów rabatowych z możliwością usunięcia, pole na nowy kod, rozbicie rabatów i kwota końcowa.

Sedno — kody rabatowe to tablica, którą podmieniasz w całości:

const { updateDiscountCodes } = useCartManager();
const applied = cart.discountCodes.map((dc) => dc.code); // aktualne kody

await updateDiscountCodes([...applied, 'LATO10']); // dodaj
await updateDiscountCodes(applied.filter((c) => c !== 'LATO10')); // usuń

Wymagania

Krok 1 — Pobierz koszyk

Operacja Cart zwraca koszyk z pozycjami, kosztami i rabatami. Wybierz kontekst renderowania:

'use client';
import { useCart } from '@/lib/graphql/hooks';

const { data, isLoading, error } = useCart({ id: '…' });

Krok 2 — Odczyt kosztów i rabatów

Koszyk niesie gotowe podsumowanie — nie licz go ręcznie:

cart.cost.subtotal       // wartość przed rabatami { amount, currencyCode }
cart.cost.totalDiscount // suma rabatów
cart.cost.total // do zapłaty
cart.discountCodes // zastosowane kody: [{ code, isApplicable }]
cart.discountAllocations // rozbicie rabatu na kod: [{ discountCode, amount }]
Connection ≠ tablica

cart.lines to Relay Connection — pozycje wyciągnij przez cart.lines.nodes. Pola cart.cost, cart.discountCodes, cart.discountAllocations są już zwykłymi obiektami/tablicami.

Krok 3 — Komponent podsumowania (React)

Kompilowany przeciw typom @doswiftly/storefront-sdk — błędne pole kosztu/rabatu lub zła sygnatura nie przejdą weryfikacji, więc przykład nie może zdryfować od API:

CartSummary.tsx
'use client';

import { useState } from 'react';
import type { Cart } from '@doswiftly/storefront-sdk';

// CartSummary: podsumowanie kosztów koszyka z obsługą kodów rabatowych —
// lista zastosowanych kodów, pole na nowy kod, rozbicie rabatów i kwota końcowa.
export function CartSummary({
cart,
onApplyDiscount,
onRemoveDiscount,
isUpdating,
}: {
cart: Cart;
onApplyDiscount: (code: string) => void | Promise<void>;
onRemoveDiscount: (code: string) => void | Promise<void>;
isUpdating?: boolean;
}) {
const [code, setCode] = useState('');
const { cost } = cart;

return (
<div>
{/* Liczba pozycji — lines to Relay Connection, rozpakowana przez .nodes */}
<p>{cart.lines.nodes.length} pozycji w koszyku</p>

{/* Zastosowane kody rabatowe + dodanie nowego */}
<div>
{cart.discountCodes.map((dc) => (
<span key={dc.code}>
{dc.code}
{dc.isApplicable ? null : ' (nieaktywny)'}
<button type="button" onClick={() => onRemoveDiscount(dc.code)}>
usuń
</button>
</span>
))}

<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Kod rabatowy"
/>
<button
type="button"
disabled={isUpdating || code.length === 0}
onClick={() => {
onApplyDiscount(code);
setCode('');
}}
>
Zastosuj
</button>
</div>

{/* Podsumowanie kosztów: wartość → rabaty → razem */}
<dl>
<div>
<dt>Wartość</dt>
<dd>
{cost.subtotal.amount} {cost.subtotal.currencyCode}
</dd>
</div>

{cart.discountAllocations.map((alloc) => (
<div key={alloc.discountCode}>
<dt>Rabat „{alloc.discountCode}"</dt>
<dd>
{alloc.amount.amount} {alloc.amount.currencyCode}
</dd>
</div>
))}

{/* Opłata za wybraną formę płatności (np. obsługa pobrania) — każdy wpis
niesie gotową etykietę. Kwota jest już wliczona w cost.total, więc
pokazujemy ją jako osobny wiersz i nigdy nie dodajemy do sumy. */}
{cost.feeAllocations.map((fee) => (
<div key={fee.label}>
<dt>{fee.label}</dt>
<dd>
{fee.amount.amount} {fee.amount.currencyCode}
</dd>
</div>
))}

<div>
<dt>Razem</dt>
<dd>
<strong>
{cost.total.amount} {cost.total.currencyCode}
</strong>
</dd>
</div>
</dl>
</div>
);
}

Krok 4 — Podłącz kody rabatowe

updateDiscountCodes z useCartManager przyjmuje całą tablicę kodów (auto-replay — kupon jest ważny niezależnie od stanu koszyka). „Dodaj" to tablica powiększona o kod, „usuń" to tablica przefiltrowana:

const { updateDiscountCodes, isLoading } = useCartManager();
const applied = cart.discountCodes.map((dc) => dc.code);

<CartSummary
cart={cart}
isUpdating={isLoading}
onApplyDiscount={(code) => updateDiscountCodes([...applied, code])}
onRemoveDiscount={(code) => updateDiscountCodes(applied.filter((c) => c !== code))}
/>;

Walidacja kodu (czy w ogóle istnieje, zanim go zastosujesz) — patrz operacja CartValidateDiscountCode. Pełna mechanika koszyka (recovery, błędy) — Koszyk.

Typy

Renderowane ze schematu GraphQL — nigdy nie rozjeżdżają się z API:

Cart

A shopping cart — the buyer-facing aggregate that holds items, totals, buyer identity, addresses, selected shipping and payment methods, gift cards and discount codes through to checkout completion.

PoleTypOpis
appliedGiftCards[CartAppliedGiftCard!]!Gift cards attached to the cart. Each card is debited by its `appliedAmount` when the cart completes.
attributes[CartAttribute!]!Cart-level custom attributes (free-form key/value). Use for cart-wide metadata such as B2B PO number or marketing source. Replace (not merge) the list with `cartUpdateAttributes`.
availablePaymentMethods[PaymentMethod!]!Available payment methods for this cart — deduplicated per type, sorted by merchant priority.
availableShippingMethodsAvailableShippingMethodsPayload!Shipping methods available for this cart at the given destination. Reads the cart subtotal and weight directly — for pre-cart preview (e.g. a product detail page calculator) use the top-level `availableShippingMethods(address, cart)` query instead. Returns an empty `methods` list plus a `DIGITAL_ONLY_NO_SHIPPING` user error when the cart has no shippable items; the storefront can use this to skip the shipping step entirely.
billingAddressMailingAddressBilling address attached to the cart via `cartSetBillingAddress`. Null when the buyer reuses the shipping address as billing (the order will then mirror the shipping address).
buyerIdentityCartBuyerIdentityBuyer identity attached to the cart (email, phone, country and language hints). Null on a fresh cart before any identity is captured.
checkoutUrlURLHosted checkout URL the storefront may redirect to as a fallback. The recommended flow is to drive checkout through SDK mutations (`cartSetShippingAddress`, `cartSelectShippingMethod`, `cartSelectPaymentMethod`, `cartComplete`).
completedOrderOrderThe order that this cart converted into. Populated only when `status` is `CONVERTED` — null on every other status. Use this to render the order confirmation page (subtotals, accessToken for guest tracking) directly off the cart you already loaded, without a second `orderByToken` round-trip.
costCartCost!Cost breakdown for the cart (subtotal, tax, shipping, discount, grand total).
createdAtDateTime!When the cart was created (ISO 8601).
discountAllocations[CartDiscountAllocation!]!Per-code discount amounts that make up `cost.totalDiscount`. Render line-item discount breakdown from this list.
discountCodes[CartDiscountCode!]!Discount codes attached to the cart with their applicability flag.
emailStringConvenience accessor — buyer email as last set via `cartUpdateBuyerIdentity`. The same value is available on `buyerIdentity.email`.
idID!Stable cart identifier — persist in a cookie / local store between sessions.
linesCartLineConnection!Lines in the cart (paginated, Relay Connection).
noteStringBuyer-supplied note (e.g. delivery instructions). Free-form, surfaced to the merchant on the order.
phoneStringConvenience accessor — buyer phone as last set via `cartUpdateBuyerIdentity`. The same value is available on `buyerIdentity.phone`.
recommendationsCartRecommendationsProduct recommendations based on cart contents
requiresShippingBoolean!True when at least one line in the cart requires physical shipping. False when every line is non-physical (digital, gift card, service, subscription). Use as the single signal to render or skip the shipping step in checkout.
selectedPaymentInstrumentStringOptional concrete instrument code selected within `selectedPaymentMethod` (e.g. `"blik"`, `"mb"`, `"154"`). Set when the buyer clicks a specific instrument tile on the storefront (per `PaymentMethod.instruments`). Pre-payment intent: copied to `Order.paymentInstrument` on `cartComplete`. Cross-reference with `availablePaymentMethods.methods[].instruments[]` to resolve `displayName` / `brandImage { url }`.
selectedPaymentMethodPaymentMethodThe payment method currently selected on the cart. Null until the buyer picks a method via `cartSelectPaymentMethod`.
selectedPaymentProviderPaymentProviderProvider the buyer picked for the selected method (the `preferredProvider` echoed back from `cartSelectPaymentMethod`). Use it to restore the exact picker tile after a reload when the storefront renders one tile per provider — `selectedPaymentMethod.type` alone cannot tell a gateway pay-by-link apart from a manual bank transfer. Null when the buyer never passed a provider.
selectedShippingMethodCartShippingMethodThe shipping method currently selected on the cart (label + cost). Null until the buyer picks a method.
shippingAddressMailingAddressShipping address attached to the cart via `cartSetShippingAddress`. Null until set.
statusCartStatus!Lifecycle status — `ACTIVE` / `RECOVERED` are editable; `ABANDONED` is a recovery flag a deliberate buyer action revives in place; `CONVERTED` / `EXPIRED` are terminal. Check this on SSR before rendering the checkout form: a `CONVERTED` cart should redirect (typically to the order confirmation when `completedOrder` is populated) instead of presenting a form whose first mutation fails with `CartErrorCode.ALREADY_COMPLETED`.
totalQuantityInt!Sum of `quantity` across all lines — the badge number for the cart icon.
updatedAtDateTime!When the cart was last modified (ISO 8601).
warnings[CartWarning!]!Non-fatal advisories computed at query time. Currently emitted: `PAYMENT_SELECTION_STALE` when `selectedPaymentMethod` / `selectedPaymentInstrument` are no longer in live gateway capabilities (storefront re-prompts the buyer). Read-only — backend state is not mutated.

CartCost

CartCostobjectPełna referencja →

Cart cost breakdown. All amounts are in the buyer preferred currency (auto-converted when the shop runs multi-currency). `total` is what the buyer will pay at checkout — no need to recompute.

PoleTypOpis
checkoutChargeMoneyDEPRECATED — do not render. Despite the name and this field history, it has never returned a surcharge: the value equals `total`, so printing it as a summary row shows the payable amount twice. Use `feeTotal` for the amount and `feeAllocations` for the rows behind it. Scheduled for removal; the value is left untouched until then so existing queries keep parsing.
checkoutChargeWithConversionPriceMoneyDEPRECATED — conversion twin of `checkoutCharge`; it shares its flaw (the value equals `total`) and its removal schedule.
feeAllocations[CartFeeAllocation!]!One entry per applied fee — what it is and how much, so the summary can name the charge instead of showing an unexplained amount. Empty when no fee applies. The sum across entries equals `feeTotal`.
feeTotalMoney!Fees charged for the selected payment method (e.g. cash-on-delivery handling), aggregated. Returns an amount of 0 when the buyer has picked no method, or the method carries no fee — so a summary row can be rendered unconditionally and simply shows zero. Already included in `total`: add it as its own row, never on top of the total.
pricesIncludeTaxBoolean!How the shop prices its catalog right now (a cart always follows the shop's current setting; an order freezes it at checkout). True: item prices include tax — render `total` with an "including tax" note. False (tax-exclusive / B2B): `subtotal` is net and tax is added on top — render `totalTax` as its own line before `total`, or the summary rows will not add up. In BOTH modes `total` is the final amount the buyer pays, tax included.
subtotalMoney!Subtotal of items in the cart before taxes, shipping and discounts.
subtotalWithConversionPriceMoneyOpt-in: subtotal with full conversion transparency.
totalMoney!Grand total the buyer will pay — includes taxes, discounts and any selected shipping. Use this directly on the checkout summary instead of summing `subtotal` + extras.
totalDiscountMoney!Aggregated discount across the cart — the sum of every entry in `discountAllocations`. Returns an amount of 0 when no discount applies.
totalDutyMoneyTotal customs duty on the cart. Null when no duty applies (most domestic orders).
totalDutyWithConversionPriceMoneyOpt-in: totalDuty with conversion transparency.
totalShippingMoneyCost of the currently selected shipping method. Null until a shipping method is selected — a selected free-shipping method returns an amount of 0, so null unambiguously means "no method chosen yet".
totalTaxMoneyTotal tax across all lines. Null when tax has not yet been calculated for this cart (e.g. before an address is set in tax-inclusive shops).
totalTaxWithConversionPriceMoneyOpt-in: totalTax with conversion transparency.
totalWithConversionPriceMoneyOpt-in: total with conversion transparency.

CartDiscountAllocation

CartDiscountAllocationobjectPełna referencja →

Per-code discount amount applied to the cart. Sum these for the aggregate total — or read `CartCost.totalDiscount`.

PoleTypOpis
amountMoney!Amount discounted by this code on the cart.
discountCodeString!The discount code that produced this allocation.

Powiązane