Przejdź do głównej zawartości

Koszt formy płatności na kafelku

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

Wybór formy płatności, przy którym kupujący od razu widzi ile realnie zapłaci: kafelek „Pobranie +5,00 zł" zamiast samego „Pobranie", a w podsumowaniu po wyborze — wiersz dopłaty z kwotą. To realna różnica w konwersji: dopłata ujawniona dopiero w podsumowaniu wygląda jak ukryty koszt, ujawniona na kafelku jest informacją.

Sedno przepisu to jedna zasada:

Kwotę dopłaty zawsze bierzesz z API, nigdy nie liczysz jej sam. Dopłata może być procentem od wartości zamówienia, więc dokładna kwota istnieje dopiero wtedy, gdy istnieje koszyk. „Zakodowany od boku" przelicznik procentu przestanie się zgadzać, gdy sklep zmieni stawkę albo kupujący użyje kodu rabatowego.

Wymagania

  • Skonfigurowany SDK — Konfiguracja Next.js.
  • Koszyk utworzony przez cartCreate — kwoty dopłat są policzone dla konkretnego koszyka, więc ten przepis zaczyna się po jego utworzeniu.
  • Sklep z włączoną opłatą za wybraną formę płatności — bez opłaty wszystko poniżej po prostu się nie pokazuje. Sama konfiguracja opłat to praca sprzedawcy w panelu; storefront nic o niej nie musi wiedzieć.

Krok 1 — Pobierz metody płatności z kwotami dopłat

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 CartAvailablePaymentMethods($cartId: ID!) {
cart(id: $cartId) {
id
availablePaymentMethods {
...PaymentMethod
}
}
}

fragment PaymentMethod on PaymentMethod {
id
name
provider
type
icon {
...ImageThumbnail
}
description
isDefault
supportedCurrencies
position
providersAvailable
preferredProvider
available
unavailableReason
instruments {
...PaymentInstrument
}
acknowledgements {
code
enforcement
statement
documents {
token
kind
url
}
}
fee {
amount {
...Money
}
label
ratePercent
}
}

fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}

fragment Money on Money {
amount
currencyCode
}

fragment PaymentInstrument on PaymentInstrument {
provider
code
type
displayName
displayHint
brandImage {
...ImageThumbnail
}
enabled
fee {
amount {
...Money
}
label
ratePercent
}
}`;

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

Każda metoda niesie pole fee z kwotą dopłaty dla tego koszyka: +5,00 zł przy ryczałcie, +2,5% (7,18 zł) przy opłacie procentowej (ratePercent jest tylko informacją pomocniczą — wiążąca jest fee.amount). Kafelek i podsumowanie korzystają z tego samego pola, więc nigdy się nie rozjadą.

fee jest też rozpisane per opcja, gdy jedna forma kryje kilka pozycji o różnych opłatach (np. poszczególne marki kart): wtedy fee samej metody jest null, a opłata wisi na każdej z instruments[].fee.

Krok 2 — Złóż kafelek metody

kafelki-platnosci.tsx
'use client';

import { useFormatPrice } from '@doswiftly/storefront-sdk/react';
import type { CartCost, PaymentMethod } from '@doswiftly/storefront-sdk';

// Dopłata za formę płatności (np. obsługę pobrania), dokładnie taką, jaką ją
// oddaje API. Kwota przychodzi już policzona DLA KONKRETNEGO koszyka —
// dopłata może być procentem od wartości zamówienia, więc przelicznika „z
// boku" nie da się odtworzyć wiernie w przeglądarce. Kwota z `fee.amount` jest
// tą samą, która po wyborze pokazuje się w podsumowaniu — kafelek i
// podsumowanie nie mogą się nigdy rozjechać.
type MethodFee = NonNullable<PaymentMethod['fee']>;

// Napis na kafelku: „+2,5% (7,18 zł)" przy opłacie procentowej, „+7,18 zł"
// przy ryczałcie. Procent (`ratePercent`) jest wyłącznie informacją
// pomocniczą przy kwocie — pokazanie go to kwestia prezentacji, nie wyliczenia.
function FeeBadge({ fee }: { fee: MethodFee }) {
const formatPrice = useFormatPrice();

if (fee.ratePercent != null) {
return <em>+{fee.ratePercent}% ({formatPrice(fee.amount)})</em>;
}
return <em>+{formatPrice(fee.amount)}</em>;
}

// Kafelek jednej formy płatności z kwotą dopłaty widoczną PRZED wyborem.
// `fee` null znaczy „ta forma nie ma dopłaty" — nie renderuj wtedy niczego.
// Nie pokazuj też „0 zł" ani „darmowe": brak dopłaty i zero złotych to dwie
// różne rzeczy w momencie, gdy klient porównuje formy płatności.
export function PaymentMethodTile({
method,
selected,
onSelect,
}: {
method: PaymentMethod;
selected?: boolean;
onSelect: () => void;
}) {
// Gdy cała forma ma jedną wspólną opłatę (np. obsługa pobrania), jest ona
// na kafelku wprost. Gdy nie — opłata może różnić się per pozycja
// (np. marka karty), wtedy każda pozycja niesie własną.
const methodFee = method.fee;
const instruments = methodFee == null ? (method.instruments ?? []) : [];

return (
<button
type="button"
onClick={onSelect}
aria-pressed={selected ?? false}
disabled={method.available === false}
>
<span>{method.name}</span>

{methodFee != null && <FeeBadge fee={methodFee} />}

{methodFee == null && instruments.some((instrument) => instrument.fee != null) && (
<ul>
{instruments.map((instrument) =>
instrument.fee == null ? null : (
<li key={instrument.code}>
{instrument.displayName}: <FeeBadge fee={instrument.fee} />
</li>
),
)}
</ul>
)}
</button>
);
}

// Pasek potwierdzenia po wyborze formy. Wiersze przychodzą z
// `cost.feeAllocations` z gotową, przetłumaczoną etykietą (tę samą pokaże
// faktura — nie twórz własnego opisu tej pozycji), a suma wszystkich dopłat
// jako `cost.feeTotal`. Kwota jest już wliczona w `cost.total`, więc pasek
// ją nazywa, NIE dodaje do niczego.
export function FeeSummaryBar({ cost }: { cost: CartCost }) {
const formatPrice = useFormatPrice();

// Brak dopłaty = brak paska. Pusty stan zamiast pokaźnego „0,00 zł".
if (cost.feeAllocations.length === 0) return null;

return (
<div role="status">
<ul>
{cost.feeAllocations.map((fee) => (
<li key={fee.label}>
{fee.label}: {formatPrice(fee.amount)}
</li>
))}
</ul>
<p>
<strong>Dopłata razem: {formatPrice(cost.feeTotal)}</strong>
</p>
</div>
);
}

Trzy rzeczy warte uwagi w tym kodzie:

  • Brak dopłaty to brak elementu, nie „0 zł". fee: null oznacza „ta forma nie ma dopłaty" — kafelek renderuje się bez napisu. Wpisywanie zamiast tego „darmowe" lub „0,00 zł" myli klienta, który porównuje formy.
  • Kwota przychodzi gotowa, także „co to znaczy dla klienta". Etykiety wierszy w podsumowaniu (feeAllocations[].label) są przetłumaczone i spójne z fakturą — nie twórz własnego opisu dopłaty.
  • Procent obok kwoty to ozdoba, nie podstawa. Format „+2,5% (7,18 zł)" czyta ratePercent tylko dla wyświetlenia; liczby wiążąca jest fee.amount.

Krok 3 — Potwierdzenie po wyborze

Kwota dopłaty wchodzi na koszyk w momencie wyboru formy — odczytasz ją z pola cost bez żadnego dodatkowego zapytania do metod:

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

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

Pasek FeeSummaryBar z kroku 2 czyta cost.feeAllocations (wiersze z etykietami) i cost.feeTotal (suma). Kupujący widzi wtedy dokładnie to, co zobaczy w zamówieniu i na fakturze.

Kwota dopłaty istnieje tylko w kontekście koszyka

Jeśli do podglądu metod płatności używasz zapytania poziomu sklepu (availablePaymentMethods — np. siatka form płatności na stronie produktu) — każda metoda i jej opcje mają tam fee: null w całości, bo kwota nie istnieje bez koszyka. To nie jest usterka.

Na kafelkach poza koszykiem nie renderuj wtedy żadnej dopłaty — nie odtwarzaj jej z ratePercent ani z własnej stałej: stawka może być procentowa, a kwota bez koszyka jest po prostu nieznana.

Typy

PaymentMethodFeeobjectPełna referencja →

Surcharge preview for choosing this payment option — render it on the picker tile (e.g. "+5 zł") BEFORE the buyer selects anything. Populated only on `Cart.availablePaymentMethods` (a percentage fee is computed from the value of this cart); always null on the shop-level `Query.availablePaymentMethods`. The amount and label match the `CartCost.feeAllocations` row that appears after selection, so the tile and the summary can never disagree.

PoleTypOpis
amountMoney!Fee amount the buyer will pay for choosing this option, in the cart currency. Tax-inclusive in a shop with tax-inclusive prices; tax is added on top in a tax-exclusive shop — the same convention as shipping.
labelString!Localized label for the fee row — identical to the `CartCost.feeAllocations[].label` shown after selection.
ratePercentFloatPercentage rate behind the fee (e.g. 2.5 = 2.5% of the cart value), so the tile can read "+2.5% (2.71 zł)". Null for a flat fee. Informational: the binding number is `amount`, already computed from this cart — never recompute it client-side.
PaymentMethodobjectPełna referencja →

A payment method offered to the buyer at checkout — what to render in the payment picker and pass to `cartSelectPaymentMethod`.

PoleTypOpis
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.
availableBoolean!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.
descriptionStringOptional buyer-facing description shown under the name (e.g. "Pay with your bank app").
feePaymentMethodFeeSurcharge 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.
iconImageIcon 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.
idID!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.
isDefaultBoolean!True when the merchant has marked this method as the default. Pre-select it in the picker.
nameString!Display name configured by the merchant (e.g. "BLIK", "Credit card", "Cash on delivery").
positionFloat!Merchant-configured display position — lower values come first in the picker.
preferredProviderPaymentProviderPreferred 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.
providerPaymentProvider!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.
typePaymentMethodType!Category of the method (CARD, BLIK, BANK_TRANSFER, INSTALLMENT, WALLET, CASH_ON_DELIVERY, OTHER). Drives iconography and copy.
unavailableReasonPaymentMethodUnavailableReasonWhen `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").
PaymentInstrumentobjectPełna referencja →

A single concrete instrument exposed by a gateway provider (e.g. BLIK code, mBank Pay-By-Link, Apple Pay) within a broader PaymentMethod. Pass `code` as `preferredInstrument` in `cartSelectPaymentMethod` to deep-link the gateway straight to this screen.

PoleTypOpis
brandImageImageOptional brand image (bank logo, wallet icon). Use as tile artwork via `brandImage { url(transform: { maxWidth: 64 }) altText }`. Null when the gateway does not expose one or the instrument has no brand visual (BLIK code).
codeString!Gateway-specific instrument identifier (PayU: `"blik"`/`"mb"`/`"c"`, P24: numeric ID string `"154"`). Pass as `preferredInstrument` in `cartSelectPaymentMethod`, ALWAYS together with `preferredProvider` — codes are scoped to their gateway and are NOT unique across providers within one method (dedupe and select by the (provider, code) pair). Stable per provider — the gateway does not renumber.
displayHintPaymentInstrumentDisplayHint!UX rendering hint — how the storefront should render this instrument (prominent button vs branded tile vs dropdown vs radio). Backend-agnostic mapping to visual treatment.
displayNameString!Buyer-facing display name (e.g. "BLIK", "mBank", "ING Bank Śląski", "Apple Pay").
enabledBoolean!True when the instrument is currently enabled in the gateway live capabilities. The storefront can gray-out the tile when false instead of hiding it (observability for the merchant).
feePaymentMethodFeeSurcharge for picking this instrument, ready to render on its tile. Null when no fee applies, when the fee cannot be determined, or on the shop-level query (`Query.availablePaymentMethods`) — only `Cart.availablePaymentMethods` carries amounts.
providerPaymentProvider!Provider that handles this instrument (UPPERCASE enum). Required for cross-provider dedupe (e.g. a BLIK code offered by both PayU and P24 — distinct instruments despite the same method type).
typePaymentInstrumentType!Semantic type classifying the instrument within the method (BLIK code vs bank vs wallet vs card brand). Storefront-facing dispatch for per-instrument UI components.
CartFeeAllocationobjectPełna referencja →

A single fee charged on the cart, with the reason it applies. Sum these for the aggregate — or read `CartCost.feeTotal`.

PoleTypOpis
amountMoney!Amount of this fee, in the cart currency.
labelString!Buyer-facing name of the charge, already translated to the shop language — render it as the summary row label. Never build your own copy for this row: what the fee is called is the merchant's decision and it also has to match the invoice.

Powiązane