Więcej funkcji SDK
Ten dokument zawiera przewodniki po dodatkowych funkcjach Storefront SDK: kolekcjach, kategoriach, kartach podarunkowych, programie lojalnościowym, zwrotach oraz multi-walutowości.
Kolekcje i kategorie
Organizacja produktów za pomocą kolekcji (flat) i kategorii (hierarchicznych).
Kluczowe różnice
| Aspekt | Kolekcje | Kategorie |
|---|---|---|
| Struktura | Płaska lista | Hierarchiczne drzewo |
| Cel | Kuratorskie grupy (np. "Bestsellery", "Nowości") | Nawigacja sklepu |
| Produkt | Może być w wielu kolekcjach | Zwykle w jednej kategorii |
| SEO | Handle dla URL | Slug z pełną ścieżką |
Kolekcje
Hooki
import { useCollection, useCollections } from '@/lib/graphql/hooks';
// Pojedyncza kolekcja z produktami
const { data } = useCollection('bestsellers');
// lub
const { data } = useCollection('gid://Collection/123');
// Lista kolekcji
const { data } = useCollections({
first: 10,
sortKey: 'TITLE', // TITLE | UPDATED_AT
reverse: false,
});
Przykład: Strona kolekcji
// app/collections/[handle]/page.tsx
import { fetchCollection } from '@/lib/graphql/server';
export default async function CollectionPage({
params,
}: {
params: { handle: string };
}) {
const { collection } = await fetchCollection(params.handle);
if (!collection) {
notFound();
}
return (
<div>
<header className="mb-8">
{collection.image && (
<img
src={collection.image.url}
alt={collection.image.altText || collection.title}
className="w-full h-64 object-cover"
/>
)}
<h1 className="text-3xl font-bold mt-4">{collection.title}</h1>
{collection.description && (
<p className="text-gray-600 mt-2">{collection.description}</p>
)}
</header>
<div className="grid grid-cols-4 gap-4">
{collection.products.edges.map(({ node: product }) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
Kategorie
Hooki
import { useCategory, useCategories } from '@/lib/graphql/hooks';
// Pojedyncza kategoria z hierarchią
const { data } = useCategory('electronics/phones');
// lub
const { data } = useCategory('gid://Category/123');
// Drzewo kategorii (korzenie + 3 poziomy w głąb)
const { data } = useCategories();
Przykład: Nawigacja kategoriami
function CategoryNav() {
const { categories } = useCategories();
return (
<nav className="space-y-2">
{categories.map((category) => (
<CategoryItem key={category.id} category={category} />
))}
</nav>
);
}
function CategoryItem({ category, depth = 0 }) {
const [isOpen, setIsOpen] = useState(depth === 0);
const hasChildren = category.children?.length > 0;
return (
<div style={{ paddingLeft: `${depth * 16}px` }}>
<div className="flex items-center gap-2">
{hasChildren && (
<button
onClick={() => setIsOpen(!isOpen)}
className="p-1"
>
{isOpen ? <ChevronDown /> : <ChevronRight />}
</button>
)}
<Link
href={`/category/${category.handle}`}
className="hover:underline"
>
{category.name}
<span className="text-gray-400 ml-1">({category.productCount})</span>
</Link>
</div>
{isOpen && hasChildren && (
<div className="mt-1">
{category.children.map((child) => (
<CategoryItem
key={child.id}
category={child}
depth={depth + 1}
/>
))}
</div>
)}
</div>
);
}
Produkty kategorii
Każda kategoria — podobnie jak kolekcja — wystawia paginowaną listę swoich produktów inline (pole products, Relay Connection). Pod stronę kategorii (/categories/[handle]) pobierz produkty w tym samym zapytaniu co dane kategorii (sortowanie i filtry przez $productsSortKey / $productsFilters), bez osobnego round-tripu. Aby zawęzić istniejącą listę produktów do kategorii, użyj filtra products(filters: [{ category: { handle } }]) — handle jest stabilny w URL i nie wymaga rozwiązywania na id. Szczegóły kontraktu: Zapytania — Kolekcje i kategorie.
Marki
Marka (producent / etykieta) to encja katalogu z własną stroną docelową — nazwą, logo, opisem, metadanymi SEO i paginowaną listą produktów marki. Różni się od filtra marki (zawężanie listy produktów — patrz Produkty — filtrowanie po marce): encja to strona marki pobierana po handle, filtr to zawężenie listy produktów. Pełny przepis na ekran marki — Przepis: Strona marki.
Operacje
Brand (strona marki) i Brands (lista marek / indeks) to operacje raw-only — brak gotowego hooka. Wywołaj je jako raw operation:
- 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 = `query Brand($id: ID, $handle: String, $productsFirst: Int = 20, $productsAfter: String, $productsSortKey: ProductSortKeys = BEST_SELLING, $productsFilters: [ProductFilter!]) {
brand(id: $id, handle: $handle) {
...Brand
products(
first: $productsFirst
after: $productsAfter
sortKey: $productsSortKey
filters: $productsFilters
) {
edges {
node {
...ProductCard
}
cursor
}
nodes {
...ProductCard
}
pageInfo {
...PageInfo
}
totalCount
}
}
}
fragment Brand on Brand {
id
handle
name
description
logo {
...ImageCard
}
seo {
title
description
}
productCount
}
fragment ImageCard on Image {
id
url(transform: {maxWidth: 800})
altText
width
height
thumbhash
}
fragment PageInfo on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
fragment ProductCard on Product {
id
handle
title
vendor
categories {
id
handle
name
}
isAvailable
averageRating
reviewCount
tags
featuredImage {
...ImageCard
}
priceRange {
minVariantPrice {
...Money
}
maxVariantPrice {
...Money
}
}
compareAtPriceRange {
minVariantPrice {
...Money
}
maxVariantPrice {
...Money
}
}
priceReduction {
...PriceReduction
}
}
fragment Money on Money {
amount
currencyCode
}
fragment PriceReduction on PriceReductionInfo {
lowestPriorPrice {
...Money
}
reductionStartedAt
referenceWindowStartedAt
referenceWindowTruncated
percentageOffLowestPrior
label
}`;
const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { id: '…', handle: '…' }, }),
});
const { data } = await res.json();
- 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 = `query Brands($first: Int = 20, $after: String, $query: String, $sortKey: BrandSortKeys = NAME, $reverse: Boolean = false) {
brands(
first: $first
after: $after
query: $query
sortKey: $sortKey
reverse: $reverse
) {
edges {
node {
...Brand
}
cursor
}
pageInfo {
...PageInfo
}
totalCount
}
}
fragment Brand on Brand {
id
handle
name
description
logo {
...ImageCard
}
seo {
title
description
}
productCount
}
fragment ImageCard on Image {
id
url(transform: {maxWidth: 800})
altText
width
height
thumbhash
}
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: { first: 20, after: '…' }, }),
});
const { data } = await res.json();
Listę marek (Brands) sortujesz przez sortKey: NAME (domyślny), PRODUCT_COUNT, CREATED_AT, UPDATED_AT; reverse: true odwraca kolejność, a opcjonalny query filtruje po nazwie marki.
Typy GraphQL
Canonical product brand entity (e.g. Funko, Nike). Distinct from ShopBrand (shop branding metadata).
| Pole | Typ | Opis |
|---|---|---|
description | String | Brand description. Returns ready-to-render HTML by default; pass `format: TEXT` for plain text or `format: JSON` for the structured document. Null when the merchant has not provided one. |
handle | String! | URL-friendly handle (e.g. "funko" → /brands/funko). Stable per-shop. |
id | ID! | Brand unique identifier |
logo | Image | Brand logo image. Use `logo { url(transform: { maxWidth: 200 }) altText }` for responsive sizing on brand tiles, breadcrumbs, and brand landing pages. |
name | String! | Brand display name (e.g. "Funko", "Nintendo") |
productCount | Int! | Number of active products carrying this brand. Use for "N products" labels on brand tiles. |
products | ProductConnection! | Products carrying this brand |
seo | SEO | SEO metadata (title, description) for the brand landing page. Null when not configured. |
updatedAt | DateTime! | Last update date (ISO 8601). |
Sort keys for the brands() query
| Wartość | Opis |
|---|---|
CREATED_AT | |
NAME | |
PRODUCT_COUNT | |
UPDATED_AT |
Karty podarunkowe
Karty podarunkowe (Gift Cards) w DoSwiftly to specjalny typ produktu (GIFT_CARD) z własnym cyklem życia: zakup, aktywacja, walidacja i realizacja w checkout.
Operacje
GiftCard (saldo/szczegóły) i GiftCardValidate (walidacja kodu) to operacje raw-only — brak gotowego hooka. Wywołaj je jako raw operation:
- 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 = `query GiftCard($code: String!) {
giftCard(code: $code) {
...GiftCard
}
}
fragment GiftCard on GiftCard {
id
maskedCode
status
initialAmount {
...Money
}
balance {
...Money
}
expiresAt
recipientName
message
createdAt
}
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: { code: '…' }, }),
});
const { data } = await res.json();
- 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 = `query GiftCardValidate($code: String!, $amount: Float) {
giftCardValidate(code: $code, amount: $amount) {
validation {
...GiftCardValidation
}
userErrors {
...UserError
}
}
}
fragment GiftCardValidation on GiftCardValidation {
isValid
availableBalance {
...Money
}
error {
...GiftCardError
}
giftCard {
...GiftCard
}
}
fragment GiftCard on GiftCard {
id
maskedCode
status
initialAmount {
...Money
}
balance {
...Money
}
expiresAt
recipientName
message
createdAt
}
fragment Money on Money {
amount
currencyCode
}
fragment GiftCardError on GiftCardError {
code
message
}
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: { code: '…', amount: 9.99 }, }),
});
const { data } = await res.json();
Zastosowanie/usunięcie karty w koszyku — przez useCartManager (SDK): applyGiftCard, removeGiftCard, updateGiftCardRecipient (patrz Koszyk).
Typy GraphQL
type GiftCard {
id: ID!
maskedCode: String! # Zamaskowany kod (np. "****-****-****-5678")
lastCharacters: String! # Ostatnie 4 znaki kodu
status: GiftCardStatus! # ACTIVE | USED | EXPIRED | DISABLED
initialAmount: Money! # Początkowa wartość karty
balance: Money! # Aktualne saldo
expiresAt: DateTime # Data wygaśnięcia (null = bez limitu)
recipientName: String # Imię odbiorcy
message: String # Wiadomość osobista
transactions: [GiftCardTransaction!]!
createdAt: DateTime!
}
Przykład: Zastosowanie karty w checkout
Zastosowanie karty w koszyku przez useCartManager().applyGiftCard (pełny lifecycle koszyka — patrz Koszyk):
'use client';
import { useState } from 'react';
import { useCartManager } from '@doswiftly/storefront-sdk/react';
function GiftCardCheckoutSection() {
const [code, setCode] = useState('');
const { applyGiftCard, status } = useCartManager();
return (
<div className="space-y-4">
<h3 className="font-medium">Karta podarunkowa</h3>
<div className="flex gap-2">
<input
type="text"
placeholder="Wpisz kod karty podarunkowej"
value={code}
onChange={(e) => setCode(e.target.value.toUpperCase())}
className="flex-1 px-3 py-2 border rounded font-mono"
/>
<button
onClick={() => applyGiftCard(code)}
disabled={!code || status.isLoading}
className="px-4 py-2 bg-primary text-white rounded disabled:opacity-50"
>
Zastosuj
</button>
</div>
</div>
);
}
Walidację salda w czasie rzeczywistym (przed zastosowaniem) zbudujesz na operacji GiftCardValidate (wyżej), opakowanej w useQuery z enabled: code.length >= 16.
Program lojalnościowy
Storefront SDK dostarcza kompletny zestaw hooków do integracji z programem lojalnościowym DoSwiftly.
Hooki
// Hooki klienckie (Client Components)
import {
useLoyaltyMember,
useLoyaltyRewards,
useLoyaltyTransactions,
useLoyaltySettings,
useReferralStats,
useRedeemLoyaltyReward,
} from '@/lib/graphql/hooks';
LoyaltyTiers, EstimatePoints (query) oraz GenerateReferralCode (mutation) nie mają gotowych hooków — to operacje raw-only:
- 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 = `query LoyaltyTiers {
loyaltyTiers {
...LoyaltyTier
}
}
fragment LoyaltyTier on LoyaltyTier {
id
name
type
minPoints
minAnnualSpend {
...Money
}
pointsMultiplier
customBenefits {
name
description
icon {
...ImageThumbnail
}
}
}
fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}
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, }),
});
const { data } = await res.json();
- 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 = `query EstimatePoints($orderTotal: Float!) {
estimatePoints(orderTotal: $orderTotal) {
...PointsEstimate
}
}
fragment PointsEstimate on PointsEstimate {
basePoints
bonusPoints
totalPoints
multiplier
}`;
const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { orderTotal: 9.99 }, }),
});
const { data } = await res.json();
- 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 GenerateReferralCode {
loyaltyGenerateReferralCode {
...GenerateReferralCodePayload
}
}
fragment GenerateReferralCodePayload on GenerateReferralCodePayload {
success
referralCode
shareUrl
userErrors {
...UserError
}
}
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, }),
});
const { data } = await res.json();
Typy GraphQL
interface LoyaltyMember {
id: string;
customerId: string;
points: LoyaltyPointsSummary;
tier: LoyaltyTier;
tierProgress: TierProgress;
annualSpend: Money;
lastActivityAt: string;
enrolledAt: string;
}
interface LoyaltyPointsSummary {
totalPoints: number; // Suma wszystkich zarobionych punktów
currentPoints: number; // Aktualnie dostępne punkty
redeemedPoints: number; // Punkty wymienione na nagrody
expiredPoints: number; // Punkty, które wygasły
expiringPoints: number; // Punkty wygasające w ciągu 30 dni
nextExpiryDate: string; // Data najbliższego wygaśnięcia
}
Przykład: Wyświetlanie punktów
'use client';
import { useLoyaltyMember, useLoyaltySettings } from '@/lib/graphql/hooks';
function PointsDisplay() {
const { data: memberData } = useLoyaltyMember();
const { data: settingsData } = useLoyaltySettings();
const member = memberData?.loyaltyMember;
const settings = settingsData?.loyaltySettings;
if (!member || !settings) return null;
const { points, tierProgress } = member;
const pointsName = settings.pointsName || 'Punkty';
return (
<div className="space-y-4 p-6 border rounded-lg">
<div className="text-center">
<p className="text-3xl font-bold">{points.currentPoints}</p>
<p className="text-muted-foreground">{pointsName} do wykorzystania</p>
</div>
{tierProgress?.nextTier && (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>{tierProgress.currentTier.name}</span>
<span>{tierProgress.nextTier.name}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary rounded-full h-2 transition-all"
style={{ width: `${Math.min(tierProgress.progressPercent, 100)}%` }}
/>
</div>
<p className="text-xs text-muted-foreground text-center">
Jeszcze {tierProgress.pointsToNextTier} {pointsName.toLowerCase()} do{' '}
{tierProgress.nextTier.name}
</p>
</div>
)}
</div>
);
}
Zwroty (RMA)
System zwrotów DoSwiftly umożliwia klientom składanie wniosków o zwrot produktów (RMA - Return Merchandise Authorization) bezpośrednio z poziomu storefront.
Operacje
Zwroty to operacje raw-only — brak gotowych hooków. Wywołaj je jako raw operations:
- 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 = `query ReturnsByOrder($orderId: ID!) {
returnsByOrder(orderId: $orderId) {
edges {
node {
...Return
}
cursor
}
pageInfo {
...PageInfo
}
totalCount
}
}
fragment PageInfo on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
fragment Return on Return {
id
returnNumber
orderId
orderNumber
status
reason
customerNote
compensationType
items {
...ReturnItem
}
refundAmount {
...Money
}
shippingLabel {
...ReturnShippingLabel
}
requestedAt
approvedAt
receivedAt
refundedAt
completedAt
createdAt
updatedAt
}
fragment Money on Money {
amount
currencyCode
}
fragment ReturnItem on ReturnItem {
id
variantId
productTitle
variantTitle
sku
image {
...ImageThumbnail
}
quantity
reason
condition
unitPrice {
...Money
}
photos {
...ReturnItemPhoto
}
status
approvedQuantity
}
fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}
fragment ReturnItemPhoto on ReturnItemPhoto {
id
url
alt
description
createdAt
}
fragment ReturnShippingLabel on ReturnShippingLabel {
url
carrier
trackingNumber
expiresAt
}`;
const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, variables: { orderId: '…' }, }),
});
const { data } = await res.json();
- 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 = `query ReturnReasons {
returnReasons {
...ReturnReasonOption
}
}
fragment ReturnReasonOption on ReturnReasonOption {
value
label
description
}`;
const res = await fetch(`${apiUrl}/storefront/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: QUERY, }),
});
const { data } = await res.json();
- 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 ReturnCreate($input: ReturnCreateInput!) {
returnCreate(input: $input) {
return {
...Return
}
userErrors {
...UserError
}
}
}
fragment Return on Return {
id
returnNumber
orderId
orderNumber
status
reason
customerNote
compensationType
items {
...ReturnItem
}
refundAmount {
...Money
}
shippingLabel {
...ReturnShippingLabel
}
requestedAt
approvedAt
receivedAt
refundedAt
completedAt
createdAt
updatedAt
}
fragment Money on Money {
amount
currencyCode
}
fragment ReturnItem on ReturnItem {
id
variantId
productTitle
variantTitle
sku
image {
...ImageThumbnail
}
quantity
reason
condition
unitPrice {
...Money
}
photos {
...ReturnItemPhoto
}
status
approvedQuantity
}
fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}
fragment ReturnItemPhoto on ReturnItemPhoto {
id
url
alt
description
createdAt
}
fragment ReturnShippingLabel on ReturnShippingLabel {
url
carrier
trackingNumber
expiresAt
}
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();
- 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 ReturnCancel($id: ID!) {
returnCancel(id: $id) {
return {
...Return
}
userErrors {
...UserError
}
}
}
fragment Return on Return {
id
returnNumber
orderId
orderNumber
status
reason
customerNote
compensationType
items {
...ReturnItem
}
refundAmount {
...Money
}
shippingLabel {
...ReturnShippingLabel
}
requestedAt
approvedAt
receivedAt
refundedAt
completedAt
createdAt
updatedAt
}
fragment Money on Money {
amount
currencyCode
}
fragment ReturnItem on ReturnItem {
id
variantId
productTitle
variantTitle
sku
image {
...ImageThumbnail
}
quantity
reason
condition
unitPrice {
...Money
}
photos {
...ReturnItemPhoto
}
status
approvedQuantity
}
fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}
fragment ReturnItemPhoto on ReturnItemPhoto {
id
url
alt
description
createdAt
}
fragment ReturnShippingLabel on ReturnShippingLabel {
url
carrier
trackingNumber
expiresAt
}
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: { id: '…' }, }),
});
const { data } = await res.json();
Typy GraphQL
interface Return {
id: string;
returnNumber: string; // Numer RMA (np. "RMA-2024-0001")
orderId: string;
orderNumber: string;
status: ReturnStatus; // REQUESTED | APPROVED | RECEIVED | REFUNDED | COMPLETED
reason: ReturnReason; // DEFECTIVE | NOT_AS_DESCRIBED | WRONG_ITEM | etc.
customerNote: string;
compensationType: CompensationType; // REFUND | STORE_CREDIT
items: ReturnItem[];
refundAmount: Money;
shippingLabel: ReturnShippingLabel;
createdAt: string;
}
enum ReturnStatus {
DRAFT
REQUESTED
APPROVED
LABEL_GENERATED
IN_TRANSIT
RECEIVED
INSPECTING
REFUND_PENDING
REFUNDED
COMPLETED
REJECTED
CANCELLED
}
enum ReturnReason {
DEFECTIVE // Produkt wadliwy
NOT_AS_DESCRIBED // Niezgodny z opisem
WRONG_ITEM // Nieprawidłowy produkt
CHANGED_MIND // Zmiana zdania
DAMAGED_SHIPPING // Uszkodzony w transporcie
OTHER // Inny powód
}
Przykład: Śledzenie zwrotu
Pobierz zwrot operacją Return (raw — opakuj w React Query po stronie klienta albo wywołaj server-side), a wynik przekaż do komponentu prezentacyjnego:
- 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 = `query Return($id: ID!) {
return(id: $id) {
return {
...Return
}
userErrors {
...UserError
}
}
}
fragment Return on Return {
id
returnNumber
orderId
orderNumber
status
reason
customerNote
compensationType
items {
...ReturnItem
}
refundAmount {
...Money
}
shippingLabel {
...ReturnShippingLabel
}
requestedAt
approvedAt
receivedAt
refundedAt
completedAt
createdAt
updatedAt
}
fragment Money on Money {
amount
currencyCode
}
fragment ReturnItem on ReturnItem {
id
variantId
productTitle
variantTitle
sku
image {
...ImageThumbnail
}
quantity
reason
condition
unitPrice {
...Money
}
photos {
...ReturnItemPhoto
}
status
approvedQuantity
}
fragment ImageThumbnail on Image {
id
url(transform: {maxWidth: 300})
altText
width
height
thumbhash
}
fragment ReturnItemPhoto on ReturnItemPhoto {
id
url
alt
description
createdAt
}
fragment ReturnShippingLabel on ReturnShippingLabel {
url
carrier
trackingNumber
expiresAt
}
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: { id: '…' }, }),
});
const { data } = await res.json();
'use client';
function ReturnTracking({ returnData }: { returnData: ReturnDetails }) {
if (!returnData) return <div>Zwrot nie znaleziony</div>;
return (
<div className="space-y-6">
<div className="flex justify-between items-start">
<div>
<h2 className="text-xl font-bold">Zwrot {returnData.returnNumber}</h2>
<p className="text-muted-foreground">
Zamówienie #{returnData.orderNumber}
</p>
</div>
<span className="px-3 py-1 bg-primary/10 text-primary rounded-full text-sm font-medium">
{returnData.status}
</span>
</div>
{/* Produkty */}
<div className="space-y-2">
<h3 className="font-medium">Zwracane produkty</h3>
{returnData.items.map((item) => (
<div key={item.id} className="flex items-center gap-3 p-3 border rounded">
{item.image?.url && (
<img src={item.image.url} alt={item.image.altText ?? item.productTitle} className="w-16 h-16 object-cover rounded" />
)}
<div className="flex-1">
<p className="font-medium">{item.productTitle}</p>
<p className="text-sm">Ilość: {item.quantity}</p>
</div>
</div>
))}
</div>
</div>
);
}
Dostepnosc per lokalizacja (StoreAvailability)
Dla sklepow z wieloma lokalizacjami (magazyny, sklepy stacjonarne, centra fulfillment) API udostepnia pole storeAvailability na ProductVariant. Pole obsluguje sortowanie po odleglosci (near), filtr typu lokalizacji oraz dyrektywe @inContext(preferredLocationId).
Kiedy uzywac
| Scenariusz | Pole do uzycia |
|---|---|
| Sklep z jedna lokalizacja | variant.isAvailable / variant.availableStock |
| Sklep z wieloma lokalizacjami | variant.storeAvailability (Connection) |
| BOPIS (odbior w sklepie) | variant.storeAvailability(locationType: STORE) |
| Sortowanie po odleglosci | variant.storeAvailability(near: { latitude, longitude }) |
| Preferowana lokalizacja | @inContext(preferredLocationId: $id) na operacji |
Dla sklepow z jedna lokalizacja pole
storeAvailabilityzwracanull. Zawsze sprawdzaj wartosc przed renderowaniem — dzieki temu storefront moze pominac UI picker'a sklepow bez dodatkowej rundy.
Token gating (publiczne vs zalogowane)
isAvailable: Boolean!— zawsze publiczne (czy wariant jest na stanie w tej lokalizacji).pickupTime: String— publiczne (zlokalizowany string konfigurowany przez sprzedawce, np. "Zwykle gotowe w ciagu 2 godz.").location: Location!— publiczne (dane adresowe, godziny otwarcia, wspolrzedne).availableStock: Int— token-gated:nulldla anonimowych,Intdla zalogowanego klienta.
Typy GraphQL
Per-location stock availability
| Pole | Typ | Opis |
|---|---|---|
availableStock | Int | Available stock at this location (computed: stock - reserved). Token-gated: null for anonymous requests, Int for authenticated customer context. |
isAvailable | Boolean! | Whether the variant is in stock at this location |
location | Location! | Location where this stock resides |
pickupTime | String | Human-readable pickup readiness string, localized (e.g., "Usually ready in 2 hours"). Null if location does not support pickup. |
Inventory location (warehouse, store, pickup point)
| Pole | Typ | Opis |
|---|---|---|
address | LocationAddress! | Physical address and coordinates |
businessHours | BusinessHours | Per-day business hours. Null when not configured — use pickupLeadTimeHours only. |
id | ID! | Location ID |
name | String! | Location display name |
pickupInstructions | String | Optional merchant pickup instructions (e.g., "Show ID at counter") |
supportsPickup | Boolean! | Whether this location supports BOPIS pickup |
timezone | String | IANA timezone identifier (e.g., "Europe/Warsaw"). Falls back to shop timezone if null. |
type | LocationType! | Location type (WAREHOUSE, STORE, etc.) |
Przyklad: Dostepnosc w sklepach stacjonarnych (BOPIS)
'use client';
function StoreAvailability({ variant }: { variant: ProductVariant }) {
// storeAvailability jest null dla sklepow z jedna lokalizacja
if (!variant.storeAvailability) {
return <p className="text-sm">{variant.isAvailable ? 'Dostepny' : 'Niedostepny'}</p>;
}
const edges = variant.storeAvailability.edges;
if (edges.length === 0) {
return <p className="text-sm text-muted-foreground">Brak sklepow stacjonarnych</p>;
}
return (
<div className="space-y-2">
<h4 className="text-sm font-medium">Odbior w sklepie</h4>
{edges.map(({ node }) => (
<div key={node.location.id} className="flex items-center justify-between text-sm">
<span>{node.location.name}</span>
<span className={node.isAvailable ? 'text-green-600' : 'text-red-500'}>
{node.isAvailable ? (node.pickupTime ?? 'Dostepny') : 'Niedostepny'}
</span>
</div>
))}
</div>
);
}
Przyklad: Sortowanie po odleglosci (near) + filtr locationType
query ProductAvailability(
$handle: String
$near: GeoCoordinateInput
) {
product(handle: $handle) {
title
variants {
id
title
isAvailable
storeAvailability(first: 5, near: $near, locationType: STORE) {
edges {
node {
isAvailable
pickupTime
location {
id
name
address { city formatted latitude longitude }
}
}
}
}
}
}
}
Przyklad: @inContext(preferredLocationId)
Pinuje preferowana lokalizacje (np. ostatnio wybrany sklep) na pierwsze miejsce w kolejnosci:
query ProductAvailability($handle: String, $loc: ID!) @inContext(preferredLocationId: $loc) {
product(handle: $handle) {
variants {
id
storeAvailability(first: 10) {
edges { node { isAvailable location { id name } } }
}
}
}
}
Query: lista lokalizacji (store picker)
Do renderowania store picker'a uzyj Query.locations:
query StorePicker($near: GeoCoordinateInput) {
locations(first: 20, hasPickupEnabled: true, near: $near) {
totalCount
pageInfo { hasNextPage endCursor }
edges {
node {
id
name
supportsPickup
address { city formatted }
}
}
}
}
Wydajnosc: Pole
storeAvailabilitypobierane jest w 2 zapytaniach SQL niezależnie od liczby wariantów (1x liczba aktywnych lokalizacji + 1x batch po wariantach); sortowanie/filtrowanie po stronie API — nie musisz optymalizowac po stronie klienta.
Multi-walutowość
Obsługa wielu walut i personalizacja wyglądu sklepu.
Konfiguracja sklepu
interface Shop {
currencyCode: string; // Domyślna waluta (np. "PLN")
supportedCurrencies: string[]; // Waluty do wyświetlania cen
paymentCurrencies: string[]; // Waluty akceptowane w płatnościach
}
Konfiguracja sklepu (fetchShop)
Shop to operacja server-only — fetchShop (brak hooka klienckiego). Po stronie klienta przekaż potrzebne pola jako props z Server Component.
import { fetchShop } from '@/lib/graphql/server';
async function CurrencyInfo() {
const { shop } = await fetchShop();
return (
<div>
<p>Domyślna waluta: {shop.currencyCode}</p>
<p>Obsługiwane: {shop.supportedCurrencies.join(', ')}</p>
<p>Płatności: {shop.paymentCurrencies.join(', ')}</p>
</div>
);
}
Currency switcher
import { useCurrencyStore } from '@/stores/currency-store';
import { useQueryClient } from '@tanstack/react-query';
// `shop` pochodzi z fetchShop (Server Component) przekazanego jako prop — Shop nie ma hooka.
function CurrencySwitcher({ shop }: { shop: ShopConfig }) {
const { currency, setCurrency } = useCurrencyStore();
const queryClient = useQueryClient();
const handleChange = (newCurrency: string) => {
setCurrency(newCurrency);
// Inwaliduj cache — ceny się zmienią
queryClient.invalidateQueries();
};
return (
<select
value={currency}
onChange={(e) => handleChange(e.target.value)}
className="border rounded px-2 py-1"
>
{shop.supportedCurrencies.map((curr) => (
<option key={curr} value={curr}>
{curr}
</option>
))}
</select>
);
}
PriceMoney type
Ceny zwracane są z informacją o konwersji:
interface PriceMoney {
amount: string; // Cena w wybranej walucie (jako string)
currencyCode: string; // Kod waluty (np. "EUR")
baseAmount: string; // Oryginalna cena w walucie sklepu
baseCurrencyCode: string; // Kod waluty bazowej sklepu
exchangeRate?: number; // Kurs wymiany (null jeśli ta sama waluta)
marginApplied?: number; // Marża zastosowana przez sklep (np. 0.02 = 2%)
rateTimestamp?: string; // Kiedy pobrano kurs
isConverted: boolean; // Czy cena jest przeliczona
}
Formatowanie cen
SDK eksportuje gotowe formattery, używaj ich zamiast pisać własne — pełna precyzja Decimal scalar (bez parseFloat), wszystkie ISO 4217 waluty, locale-correct symbol.
import { formatPrice } from '@doswiftly/storefront-sdk';
const formattedPrice = formatPrice(product.priceRange.minVariantPrice, 'pl-PL');
// "123,45 zł"
const inEnglish = formatPrice(product.priceRange.minVariantPrice, 'en-US');
// "PLN 123.45"
W obrębie <StorefrontProvider> użyj hooka useFormatPrice() — locale jest auto-resolved z useLanguageStore:
'use client';
import { useFormatPrice } from '@doswiftly/storefront-sdk/react';
function ProductPrice({ product }) {
const formatPrice = useFormatPrice();
return <span>{formatPrice(product.priceRange.minVariantPrice)}</span>;
}
Debugowanie requestów (od v17.2.0)
createStorefrontClient({ debug }) przyjmuje tagged union — boolean | 'verbose' | DebugOptions — dla pełnego dumpa request/response w konsoli (lub własnym sink'u). Backward-compat: debug: true zachowuje minimalne logi z wcześniejszych wersji.
Tryby
| Wartość | Logi |
|---|---|
false / pominięte | brak |
true (backward-compat) | operationName + variables (request), status + hasErrors + userErrors (response) |
'verbose' | wszystkie wymiary: pełna query, variables, request headers (redacted), response body (data + errors + extensions), response headers, durationMs, flatuje userErrors[] z mutacji |
DebugOptions object | granular opt-in per wymiar |
DebugOptions
interface DebugOptions {
request?: boolean; // pełna query string (default false)
response?: boolean; // pełna response body (default false)
headers?: boolean; // request + response headers (default false)
timing?: boolean; // durationMs (default false)
userErrors?: boolean; // flat userErrors[] z mutacji (default true)
log?: (event: DebugEvent) => void; // custom sink (default console.log)
}
interface DebugEvent {
phase: 'request' | 'response';
operationName: string | undefined;
data: Record<string, unknown>;
}
Redact bezwarunkowy
Gdy headers jest włączone, SDK zawsze maskuje credentials niezależnie od trybu:
Authorization: Bearer <token>→Authorization: Bearer ***<last4>- Cookie
customerAccessToken=<value>→customerAccessToken=***<last4>(zarówno w requestCookiejak responseSet-Cookie).
Token nigdy nie pojawia się w pełni w żadnym logu — nawet w verbose mode w środowisku, gdzie developer celowo włączył pełen debug.
Env var fallback
# .env.local
DOSWIFTLY_SDK_DEBUG=verbose # lub true / 1 / minimal
Aktywuje wybrany preset gdy debug nie jest ustawione w kodzie. Env var jest ignorowany w NODE_ENV=production (PII safety) — w produkcji wyłącznie explicit debug w kodzie ma efekt (operator-driven traces).
Przykład — routing przez pino
import { createStorefrontClient } from '@doswiftly/storefront-sdk';
import pino from 'pino';
const logger = pino({ name: 'storefront' });
const client = createStorefrontClient({
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
debug: {
request: true,
response: true,
headers: true,
timing: true,
log: (event) =>
logger[event.phase === 'request' ? 'debug' : 'info']({
op: event.operationName,
...event.data,
}),
},
});
Quick start dev
const client = createStorefrontClient({
apiUrl,
shopSlug,
debug: 'verbose', // pełen dump do console.log
});
Wysyłka logów do backendu (debug.remote)
Oprócz lokalnego sink'a (console.log / własny log), SDK może wysyłać te same zdarzenia debug do Twojego backendu obserwowalności. Dzięki temu zobaczysz request/response storefrontu (operacja, status, userErrors, timing, variables) w centralnym stosie logów — zamiast prosić każdego użytkownika o otwarcie DevTools. To opt-in i fire-and-forget: nieudana wysyłka jest połykana, więc telemetria nigdy nie wpłynie na żądanie, które ją wygenerowało.
DebugOptions.remote przyjmuje boolean | RemoteDebugOptions:
import { createStorefrontClient } from '@doswiftly/storefront-sdk';
const client = createStorefrontClient({
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
debug: {
timing: true, // wymiary i tak rządzą tym, CO jest wysyłane (patrz niżej)
remote: true, // włącz wysyłkę z domyślnymi ustawieniami
},
});
interface RemoteDebugOptions {
endpoint?: string; // domyślnie `${apiUrl}/storefront/debug-logs`
batchSize?: number; // flush po N zdarzeniach (default 10)
flushIntervalMs?: number; // flush po N ms nawet jeśli batch niepełny (default 5000)
sessionId?: string; // własny identyfikator korelacji (default wygenerowany)
sdkVersion?: string; // opcjonalna etykieta wersji buildu dołączana do batcha
}
Tryb debug ze zmiennej środowiskowej
Sam tryb debug (co jest logowane lokalnie) możesz włączyć flagą DOSWIFTLY_SDK_DEBUG, bez dotykania kodu. W produkcji (NODE_ENV=production) jest ignorowana — chroni przed przypadkowym logowaniem danych klienta:
# .env.local
DOSWIFTLY_SDK_DEBUG=verbose # pełny tryb debug lokalnie ('true'/'1'/'minimal' = zestaw minimalny)
Zmienna środowiskowa steruje wyłącznie trybem debug — nie włącza wysyłki zdalnej. Wysyłkę do backendu aktywujesz jedynie programowo przez debug: { remote: true } (kod wyżej) lub przekazując gotowy transport (patrz Jeden wspólny kanał debug).
Co jest wysyłane
To samo, co lokalny tryb — remote: true bez dodatkowych wymiarów wysyła zestaw minimalny: variables operacji + status odpowiedzi + userErrors[]. Włączenie response / headers / timing poszerza payload. Każdy batch niesie też sessionId (korelacja w obrębie jednej sesji klienta) oraz sdkVersion, a SDK wyłuskuje cartId / orderId z variables, żeby log był filtrowalny bez parsowania całego payloadu.
Mechanika transportu
- Batchowanie — zdarzenia są buforowane i wysyłane gdy bufor osiągnie
batchSize, poflushIntervalMs, albo gdy strona jest ukrywana / zamykana (pagehide/visibilitychange→hidden). Ostatni przypadek ratuje zdarzenia w flow z przekierowaniem (np. hand-off do bramki płatności), które inaczej by przepadły. fetch(..., { keepalive: true })— pozwala dokończyć wysyłkę podczas unload strony.credentials: 'omit'— to telemetria, nie żądanie uwierzytelnione; ciasteczka nie są wysyłane. Sklep identyfikuje nagłówekX-Shop-Slug(backend go waliduje).- 0 zależności runtime — transport używa wyłącznie
fetch,DateiglobalThis.crypto, więc działa też poza przeglądarką (Node / Edge / Deno), gdzie flush na unload jest pomijany.
Bezpieczeństwo i PII
Authorization: Bearer <token>i cookiecustomerAccessToken=<value>są bezwarunkowo maskowane do***<last4>— identycznie jak w logach lokalnych — zanim cokolwiek opuści przeglądarkę.- Backend dodatkowo redaguje payload przed zapisem (e-mail →
j***@e***.com, sekrety infrastruktury, adresy IP) — defense-in-depth na wypadek PII wvariables. - Wysyłka jest opt-in (
remotedomyślniefalse); bez włączenia SDK nie kontaktuje się z endpointem debug.
Gdzie lądują zdarzenia
Zdarzenia trafiają do Twojego backendu DoSwiftly (${apiUrl}/storefront/debug-logs) i są zapisywane jako ustrukturyzowane linie logów w dedykowanym strumieniu obserwowalności (source=storefront-sdk). Zdarzenia z błędami (hasErrors, userErrors[], status HTTP ≥ 400) są logowane na poziomie warn, czyste na info.
Zdarzenia cyklu życia cookie (phase: 'cookie')
Oprócz GraphQL request/response SDK potrafi emitować zdarzenia ustawiania i kasowania ciasteczek, którymi sam zarządza: cart-id, preferred-currency, preferred-language. Dzięki temu zobaczysz kiedy ciasteczko zostało zapisane lub usunięte na tej samej osi czasu co operacje GraphQL — np. moment, w którym complete() czyści cart-id po sfinalizowaniu zamówienia.
Zdarzenie cookie ma phase: 'cookie', operationName: undefined, a w data:
interface CookieDebugData {
name: 'cart-id' | 'preferred-currency' | 'preferred-language';
action: 'set' | 'clear';
value?: string; // tylko dla 'set' — wartość ciasteczka (np. cartId, kod waluty)
maxAge?: number; // tylko dla 'set' — TTL w sekundach
}
Wartości tych ciasteczek (cart-id, kod waluty, kod języka) nie są PII, więc przechodzą bez redakcji. Tokeny w nagłówkach są maskowane przez SDK, a variables / body i wartości wrażliwe redaguje dodatkowo backend (patrz Bezpieczeństwo i PII).
Jeden wspólny kanał debug (createRemoteDebugTransport)
GraphQL i wszystkie trzy ciasteczka mają osobne punkty wstrzyknięcia sink'u. Żeby uzyskać jeden spójny timeline z jednym sessionId (GraphQL + cookie set/clear przeplatane), utwórz jeden transport i wstrzyknij go we wszystkie miejsca:
import {
createStorefrontClient,
createRemoteDebugTransport,
} from '@doswiftly/storefront-sdk';
const apiUrl = process.env.NEXT_PUBLIC_API_URL!;
const shopSlug = process.env.NEXT_PUBLIC_SHOP_SLUG!;
// 1. Jeden transport = jeden sessionId dla całej sesji klienta.
const debugTransport = createRemoteDebugTransport({
endpoint: `${apiUrl}/storefront/debug-logs`,
shopSlug,
fetch,
});
// 2. GraphQL — przekaż gotowy transport jako `debug.remote`
// (`remote` przyjmuje teraz boolean | RemoteDebugOptions | RemoteDebugSink).
const client = createStorefrontClient({
apiUrl,
shopSlug,
debug: {
timing: true,
remote: debugTransport, // ten sam kanał co cookies poniżej
},
});
// 3. Currency + language (StorefrontProvider) — prop `cookieDebug`
import { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
<StorefrontProvider
shopData={shopData}
cookieDebug={debugTransport.capture}
>
{children}
</StorefrontProvider>;
// 4. Cart-id — wstrzyknij tam, gdzie tworzysz cookie store cart:
// a) gdy używasz menedżera koszyka SDK:
const cart = useCartManager({ cookieDebug: debugTransport.capture });
// lub <CartManagerProvider cookieDebug={debugTransport.capture}>
// b) gdy budujesz cookie store ręcznie (np. własny runner odzyskiwania):
import { createBrowserCartCookieStore } from '@doswiftly/storefront-sdk/react';
const cookieStore = createBrowserCartCookieStore({
onDebug: debugTransport.capture,
});
Po podpięciu wszystkich czterech źródeł jeden sessionId w logach daje pełny przeplatany obraz sesji: każde zapytanie GraphQL plus każde set/clear na cart-id / preferred-currency / preferred-language.
RemoteDebugSinkto minimalny interfejs ({ capture(event) }).createRemoteDebugTransportzwraca taki obiekt —transport.capturejest stabilną referencją, możesz ją przekazać bezpośrednio jakocookieDebug. Twórz transport raz (np. w module / na poziomie aplikacji), nie przy każdym renderze — listenery flush napagehide/visibilitychangerejestrują się na czas życia transportu.
Następne kroki
- Przegląd SDK — Architektura i koncepcja SDK
- Produkty — Wyświetlanie i wyszukiwanie produktów
- Koszyk — Zarządzanie koszykiem zakupów
- API Reference — Szczegółowa dokumentacja API