Koszyk z rabatami
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
- Skonfigurowany SDK i provider — Konfiguracja Next.js.
- Podstawy koszyka (
useCartManager) — Koszyk.
Krok 1 — Pobierz koszyk
Operacja Cart zwraca koszyk z pozycjami, kosztami i rabatami. Wybierz kontekst renderowania:
- 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();
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 }]
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:
'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.
| Pole | Typ | Opis |
|---|---|---|
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. |
availableShippingMethods | AvailableShippingMethodsPayload! | 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. |
billingAddress | MailingAddress | Billing 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). |
buyerIdentity | CartBuyerIdentity | Buyer identity attached to the cart (email, phone, country and language hints). Null on a fresh cart before any identity is captured. |
checkoutUrl | URL | Hosted checkout URL the storefront may redirect to as a fallback. The recommended flow is to drive checkout through SDK mutations (`cartSetShippingAddress`, `cartSelectShippingMethod`, `cartSelectPaymentMethod`, `cartComplete`). |
completedOrder | Order | The 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. |
cost | CartCost! | Cost breakdown for the cart (subtotal, tax, shipping, discount, grand total). |
createdAt | DateTime! | 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. |
email | String | Convenience accessor — buyer email as last set via `cartUpdateBuyerIdentity`. The same value is available on `buyerIdentity.email`. |
id | ID! | Stable cart identifier — persist in a cookie / local store between sessions. |
lines | CartLineConnection! | Lines in the cart (paginated, Relay Connection). |
note | String | Buyer-supplied note (e.g. delivery instructions). Free-form, surfaced to the merchant on the order. |
phone | String | Convenience accessor — buyer phone as last set via `cartUpdateBuyerIdentity`. The same value is available on `buyerIdentity.phone`. |
recommendations | CartRecommendations | Product recommendations based on cart contents |
requiresShipping | Boolean! | 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. |
selectedPaymentInstrument | String | Optional 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 }`. |
selectedPaymentMethod | PaymentMethod | The payment method currently selected on the cart. Null until the buyer picks a method via `cartSelectPaymentMethod`. |
selectedPaymentProvider | PaymentProvider | Provider 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. |
selectedShippingMethod | CartShippingMethod | The shipping method currently selected on the cart (label + cost). Null until the buyer picks a method. |
shippingAddress | MailingAddress | Shipping address attached to the cart via `cartSetShippingAddress`. Null until set. |
status | CartStatus! | 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`. |
totalQuantity | Int! | Sum of `quantity` across all lines — the badge number for the cart icon. |
updatedAt | DateTime! | 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
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.
| Pole | Typ | Opis |
|---|---|---|
checkoutCharge | Money | DEPRECATED — 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. |
checkoutChargeWithConversion | PriceMoney | DEPRECATED — 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`. |
feeTotal | Money! | 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. |
pricesIncludeTax | Boolean! | 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. |
subtotal | Money! | Subtotal of items in the cart before taxes, shipping and discounts. |
subtotalWithConversion | PriceMoney | Opt-in: subtotal with full conversion transparency. |
total | Money! | 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. |
totalDiscount | Money! | Aggregated discount across the cart — the sum of every entry in `discountAllocations`. Returns an amount of 0 when no discount applies. |
totalDuty | Money | Total customs duty on the cart. Null when no duty applies (most domestic orders). |
totalDutyWithConversion | PriceMoney | Opt-in: totalDuty with conversion transparency. |
totalShipping | Money | Cost 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". |
totalTax | Money | Total 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). |
totalTaxWithConversion | PriceMoney | Opt-in: totalTax with conversion transparency. |
totalWithConversion | PriceMoney | Opt-in: total with conversion transparency. |
CartDiscountAllocation
Per-code discount amount applied to the cart. Sum these for the aggregate total — or read `CartCost.totalDiscount`.
| Pole | Typ | Opis |
|---|---|---|
amount | Money! | Amount discounted by this code on the cart. |
discountCode | String! | The discount code that produced this allocation. |
Powiązane
- Podstawy koszyka, mutacje, recovery — Koszyk.
- Strona produktu (dodawanie do koszyka) — Strona produktu z wariantami.
- Rdzeń SDK (klienty, store) — Referencja TypeScript SDK.
- Kontrakt operacji
Cart/CartDiscountCodesUpdate— Referencja GraphQL API.