Przejdź do głównej zawartości

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

AspektKolekcjeKategorie
StrukturaPłaska listaHierarchiczne drzewo
CelKuratorskie grupy (np. "Bestsellery", "Nowości")Nawigacja sklepu
ProduktMoże być w wielu kolekcjachZwykle w jednej kategorii
SEOHandle dla URLSlug 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:

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();

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).

PoleTypOpis
descriptionStringBrand 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.
handleString!URL-friendly handle (e.g. "funko" → /brands/funko). Stable per-shop.
idID!Brand unique identifier
logoImageBrand logo image. Use `logo { url(transform: { maxWidth: 200 }) altText }` for responsive sizing on brand tiles, breadcrumbs, and brand landing pages.
nameString!Brand display name (e.g. "Funko", "Nintendo")
productCountInt!Number of active products carrying this brand. Use for "N products" labels on brand tiles.
productsProductConnection!Products carrying this brand
seoSEOSEO metadata (title, description) for the brand landing page. Null when not configured.
updatedAtDateTime!Last update date (ISO 8601).
BrandSortKeysenumPełna referencja →

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:

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();

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:

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();

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();

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:

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();

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();

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();

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:

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

ScenariuszPole do uzycia
Sklep z jedna lokalizacjavariant.isAvailable / variant.availableStock
Sklep z wieloma lokalizacjamivariant.storeAvailability (Connection)
BOPIS (odbior w sklepie)variant.storeAvailability(locationType: STORE)
Sortowanie po odlegloscivariant.storeAvailability(near: { latitude, longitude })
Preferowana lokalizacja@inContext(preferredLocationId: $id) na operacji

Dla sklepow z jedna lokalizacja pole storeAvailability zwraca null. 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: Stringpubliczne (zlokalizowany string konfigurowany przez sprzedawce, np. "Zwykle gotowe w ciagu 2 godz.").
  • location: Location!publiczne (dane adresowe, godziny otwarcia, wspolrzedne).
  • availableStock: Inttoken-gated: null dla anonimowych, Int dla zalogowanego klienta.

Typy GraphQL

StoreAvailabilityobjectPełna referencja →

Per-location stock availability

PoleTypOpis
availableStockIntAvailable stock at this location (computed: stock - reserved). Token-gated: null for anonymous requests, Int for authenticated customer context.
isAvailableBoolean!Whether the variant is in stock at this location
locationLocation!Location where this stock resides
pickupTimeStringHuman-readable pickup readiness string, localized (e.g., "Usually ready in 2 hours"). Null if location does not support pickup.
LocationobjectPełna referencja →

Inventory location (warehouse, store, pickup point)

PoleTypOpis
addressLocationAddress!Physical address and coordinates
businessHoursBusinessHoursPer-day business hours. Null when not configured — use pickupLeadTimeHours only.
idID!Location ID
nameString!Location display name
pickupInstructionsStringOptional merchant pickup instructions (e.g., "Show ID at counter")
supportsPickupBoolean!Whether this location supports BOPIS pickup
timezoneStringIANA timezone identifier (e.g., "Europe/Warsaw"). Falls back to shop timezone if null.
typeLocationType!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 storeAvailability pobierane 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-onlyfetchShop (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ętebrak
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 objectgranular 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 request Cookie jak response Set-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)
notatka

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, po flushIntervalMs, albo gdy strona jest ukrywana / zamykana (pagehide / visibilitychangehidden). 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łówek X-Shop-Slug (backend go waliduje).
  • 0 zależności runtime — transport używa wyłącznie fetch, Date i globalThis.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 cookie customerAccessToken=<value>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 w variables.
  • Wysyłka jest opt-in (remote domyślnie false); 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.

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.

RemoteDebugSink to minimalny interfejs ({ capture(event) }). createRemoteDebugTransport zwraca taki obiekt — transport.capture jest stabilną referencją, możesz ją przekazać bezpośrednio jako cookieDebug. Twórz transport raz (np. w module / na poziomie aplikacji), nie przy każdym renderze — listenery flush na pagehide / visibilitychange rejestrują się na czas życia transportu.

Następne kroki