@behio/storefront-sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1393 +1,120 @@
1
1
  # @behio/storefront-sdk
2
2
 
3
- **Build your own e-shop. Keep 100% control over the frontend.**
3
+ **Headless e-commerce SDK for building custom storefronts.**
4
4
 
5
- Behio is a headless e-commerce platform that gives you a complete backend — products, inventory, orders, customers, discounts, multi-currency, multi-language, payments — and lets *you* design the storefront however you want. No themes, no templates, no vendor lock-in.
6
-
7
- This SDK is the fastest way to connect your Next.js, React, or any JavaScript app to the Behio Storefront API.
5
+ Behio gives you a complete e-commerce backend — products, inventory, orders, customers, discounts, multi-currency, multi-language — and lets you design the storefront however you want. No themes, no templates, no vendor lock-in.
8
6
 
9
7
  ## Why Behio?
10
8
 
11
- - **You own the frontend.** Build it in Next.js, Astro, Nuxt, SvelteKit, React Native — or use plain HTML. The backend doesn't care.
12
- - **Production-ready e-commerce backend in minutes.** Product catalog, cart, checkout, customer accounts, orders, tracking, CMS pages, discount codes, volume pricing, multi-warehouse inventory all included.
13
- - **Built for developers.** Type-safe, auto-completing, fully documented. Modern React hooks with TanStack Query under the hood. Automatic token refresh, optimistic cart updates, SSR prefetch support.
14
- - **Scale-ready.** Redis caching, rate limiting, webhooks, API keys with scoped permissions. From MVP to millions of orders on the same stack.
15
- - **Admin UI you don't have to build.** Behio comes with a full admin dashboard for managing products, orders, customers, CMS content, and analytics. You focus on the customer experience.
16
-
17
- **Perfect for:** agencies building custom e-shops, brands that want a unique storefront, SaaS apps that need built-in commerce, headless migrations from Shopify / WooCommerce.
18
-
19
- ---
20
-
21
- ## 🚀 Get started
22
-
23
- Pick your integration path:
24
-
25
- ### [👉 Next.js / React — with hooks, auto-caching, SSR](#-nextjs--react-path)
26
- The fastest way. React hooks for everything, automatic state management, SSR prefetch built-in.
27
-
28
- ### [👉 Vanilla JavaScript / Node.js — core client only](#-vanilla-js--nodejs-path)
29
- Works anywhere: browsers, Node.js, Deno, Bun, Cloudflare Workers, edge runtimes. No React required.
30
-
31
- ---
32
-
33
- ## 📚 Table of contents
34
-
35
- **Setup**
36
- - [Install](#install)
37
- - [Get your API key](#get-your-api-key)
38
-
39
- **Integration paths**
40
- - [Next.js / React path](#-nextjs--react-path) → [Setup provider](#1-setup-the-provider) → [Fetch data](#2-fetch-data-with-hooks) → [Cart & checkout](#3-cart--checkout) → [Authentication](#4-customer-authentication) → [Server Actions](#5-server-actions--forms)
41
- - [Vanilla JS path](#-vanilla-js--nodejs-path) → [Create client](#1-create-a-client) → [Use it](#2-call-any-endpoint)
42
-
43
- **Reference**
44
- - [All React hooks](#all-react-hooks)
45
- - [All API methods](#all-api-methods)
46
- - [Types](#types)
47
- - [Constants / Enums](#constants--enums)
48
- - [Error handling](#error-handling)
49
- - [Events & interceptors](#events--interceptors)
50
- - [Auto token refresh](#auto-token-refresh)
51
- - [Cart session management](#cart-session-management)
52
-
53
- ---
9
+ - **You own the frontend.** Next.js, React, Vue, Nuxt, Astro, or plain JS the backend doesn't care.
10
+ - **Production-ready in minutes.** Catalog, cart, checkout, customer accounts, orders, CMS, discount codes, gift cards, loyalty programs, and more.
11
+ - **Built for developers.** Full TypeScript types, auto-completing, modern React hooks with TanStack Query.
12
+ - **Scale-ready.** Redis caching, rate limiting, webhooks, atomic checkout (no double-spend, no overselling).
54
13
 
55
14
  ## Install
56
15
 
57
16
  ```bash
58
17
  npm install @behio/storefront-sdk
59
- # or
60
- yarn add @behio/storefront-sdk
61
- # or
62
- pnpm add @behio/storefront-sdk
63
- ```
64
-
65
- The SDK has two entry points:
66
-
67
- ```typescript
68
- // Core — works everywhere (Node.js, browser, Deno, Bun, edge runtimes)
69
- import { BehioStorefront } from '@behio/storefront-sdk';
70
-
71
- // React — hooks + provider for Next.js / React apps
72
- import { BehioProvider, useProducts, useCart, useAuth } from '@behio/storefront-sdk/react';
73
- ```
74
-
75
- ## Get your API key
76
-
77
- 1. Log in to your Behio admin dashboard
78
- 2. Go to your e-shop → **Settings** → **API Keys**
79
- 3. Create a **Public key** (starts with `pk_live_`) — safe for browsers
80
- 4. Optionally create a **Private key** (starts with `sk_live_`) — for server-side only
81
-
82
- The API key identifies your e-shop. No shop domain needed.
83
-
84
- ---
85
-
86
- # 🎯 Next.js / React path
87
-
88
- You can build your Next.js storefront in **two ways** — pick one or mix them:
89
-
90
- ### [A. Client-side hooks](#1-setup-the-provider) 🪝
91
- `BehioProvider` + hooks like `useProducts`, `useCart`, `useAuth`. Fast interactivity, automatic state management, optimistic updates.
92
-
93
- **Best for:** SPAs, logged-in dashboards, product browsing, cart UI, live filters.
94
- **Downside:** API key is public (uses `NEXT_PUBLIC_BEHIO_API_KEY`), runs in the browser.
95
-
96
- ### [B. Server Components + Server Actions](#b-server-components--server-actions) 🔒
97
- Use the core `BehioStorefront` directly in RSC and server actions. API key never leaves the server, tokens in httpOnly cookies, forms work without JS.
98
-
99
- **Best for:** SEO-critical pages, checkout, auth forms, sensitive mutations.
100
- **Upside:** More secure — private API key, better perceived performance, progressive enhancement.
101
-
102
- ### Recommendation
103
-
104
- Mix both:
105
- - **Server Components** for the first paint (product pages, categories, SEO)
106
- - **Client hooks** for interactive bits (cart, search, filters, logged-in area)
107
- - **Server Actions** for anything sensitive (checkout, auth, customer profile)
108
-
109
- ---
110
-
111
- ## 1. Setup the provider
112
-
113
- Create a client wrapper for the provider:
114
-
115
- ```tsx
116
- // app/providers.tsx
117
- 'use client';
118
- import { BehioProvider } from '@behio/storefront-sdk/react';
119
-
120
- export function Providers({ children }: { children: React.ReactNode }) {
121
- return (
122
- <BehioProvider
123
- apiKey={process.env.NEXT_PUBLIC_BEHIO_API_KEY!} // e.g. "pk_live_6d3f255..."
124
- locale="cs"
125
- currency="CZK"
126
- storage="cookies" // "cookies" | "localStorage" | "memory"
127
- >
128
- {children}
129
- </BehioProvider>
130
- );
131
- }
132
- ```
133
-
134
- Use it in your root layout (stays as a server component):
135
-
136
- ```tsx
137
- // app/layout.tsx
138
- import { Providers } from './providers';
139
-
140
- export default function RootLayout({ children }: { children: React.ReactNode }) {
141
- return (
142
- <html>
143
- <body>
144
- <Providers>{children}</Providers>
145
- </body>
146
- </html>
147
- );
148
- }
149
- ```
150
-
151
- Set your env variables:
152
-
153
- ```bash
154
- # .env.local
155
- NEXT_PUBLIC_BEHIO_API_KEY=pk_live_xxxxxxxxxxxx
156
-
157
- # For server-side (Server Actions, RSC) — NEVER expose to client
158
- BEHIO_API_KEY=sk_live_xxxxxxxxxxxx
159
- ```
160
-
161
- ## 2. Fetch data with hooks
162
-
163
- All catalog data available via React hooks. Each hook returns `{ data, isLoading, error, refetch }`.
164
-
165
- ```tsx
166
- 'use client';
167
- import { useShopInfo, useProducts, useProduct, useCategories, formatPrice } from '@behio/storefront-sdk/react';
168
-
169
- export function HomePage() {
170
- const { data: shop } = useShopInfo();
171
- const { data: products } = useProducts({ limit: 12, sort: 'newest' });
172
- const { data: cats } = useCategories();
173
-
174
- return (
175
- <div>
176
- <h1>{shop?.name}</h1>
177
- <nav>
178
- {cats?.map(c => <a key={c.id} href={`/category/${c.slug}`}>{c.name}</a>)}
179
- </nav>
180
- <div className="grid grid-cols-4 gap-4">
181
- {products?.items.map(p => (
182
- <div key={p.id}>
183
- <h3>{p.name}</h3>
184
- <p>{formatPrice(p.price.amount, p.price.currency)}</p>
185
- </div>
186
- ))}
187
- </div>
188
- </div>
189
- );
190
- }
191
18
  ```
192
19
 
193
- **SSR prefetch** (App Router server components):
20
+ ## Quick Start
194
21
 
195
22
  ```tsx
196
- // app/products/[slug]/page.tsx (server component)
197
23
  import { BehioStorefront } from '@behio/storefront-sdk';
198
- import { ProductDetail } from './ProductDetail';
199
24
 
200
- export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
201
- const { slug } = await params;
202
- const shop = new BehioStorefront({
203
- apiKey: process.env.BEHIO_API_KEY!,
204
- });
205
- const initialData = await shop.catalog.getProduct(slug);
206
- return <ProductDetail slug={slug} initialData={initialData} />;
207
- }
208
- ```
209
-
210
- ```tsx
211
- // ProductDetail.tsx (client component — hydrates with SSR data)
212
- 'use client';
213
- import { useProduct } from '@behio/storefront-sdk/react';
214
-
215
- export function ProductDetail({ slug, initialData }) {
216
- const { data: product } = useProduct(slug, { initialData });
217
- return <h1>{product?.name}</h1>;
218
- }
219
- ```
220
-
221
- → [All available hooks](#all-react-hooks)
222
-
223
- ## 3. Cart & checkout
224
-
225
- `useCart` gives you everything — state, mutations, computed values, optimistic updates.
226
-
227
- ```tsx
228
- 'use client';
229
- import { useCart, useCartCount, formatPrice } from '@behio/storefront-sdk/react';
230
-
231
- export function CartButton() {
232
- const count = useCartCount(); // lightweight — just the number
233
- return <span>Cart ({count})</span>;
234
- }
235
-
236
- export function CartPage() {
237
- const {
238
- cart, isEmpty, itemCount,
239
- addItem, updateQuantity, removeItem, clear,
240
- applyDiscount, removeDiscount,
241
- } = useCart();
242
-
243
- if (isEmpty) return <p>Your cart is empty</p>;
244
-
245
- return (
246
- <div>
247
- {cart?.items.map(item => (
248
- <div key={item.id}>
249
- <span>{item.product.name}</span>
250
- <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
251
- <span>{item.quantity}</span>
252
- <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
253
- <button onClick={() => removeItem(item.id)}>Remove</button>
254
- </div>
255
- ))}
256
- <p>Total: {formatPrice(cart?.grandTotal ?? 0, cart?.currency ?? 'CZK')}</p>
257
- </div>
258
- );
259
- }
260
- ```
261
-
262
- Checkout:
263
-
264
- ```tsx
265
- 'use client';
266
- import { useCheckout } from '@behio/storefront-sdk/react';
267
-
268
- export function CheckoutForm() {
269
- const { createOrder, isCreating, order } = useCheckout();
270
-
271
- if (order) return <h2>Order #{order.orderNumber} confirmed!</h2>;
272
-
273
- return (
274
- <form onSubmit={async (e) => {
275
- e.preventDefault();
276
- await createOrder({
277
- email: 'customer@example.com',
278
- shippingAddress: { firstName: 'Jan', lastName: 'Novak', street: 'Hlavni 1', city: 'Praha', zip: '11000', country: 'CZ' },
279
- billingAddress: { firstName: 'Jan', lastName: 'Novak', street: 'Hlavni 1', city: 'Praha', zip: '11000', country: 'CZ' },
280
- });
281
- }}>
282
- {/* form fields */}
283
- <button disabled={isCreating}>{isCreating ? 'Processing...' : 'Place order'}</button>
284
- </form>
285
- );
286
- }
287
- ```
288
-
289
- → [Cart session management](#cart-session-management)
290
-
291
- ## 4. Customer authentication
292
-
293
- Register, login, logout — with automatic token persistence and anonymous cart merge.
294
-
295
- ```tsx
296
- 'use client';
297
- import { useAuth } from '@behio/storefront-sdk/react';
298
-
299
- export function AuthSection() {
300
- const { isLoggedIn, customer, login, logout, isLoggingIn, loginError } = useAuth();
301
-
302
- if (isLoggedIn) {
303
- return (
304
- <>
305
- <p>Welcome, {customer?.firstName || customer?.email}</p>
306
- <button onClick={logout}>Logout</button>
307
- </>
308
- );
309
- }
310
-
311
- return (
312
- <form onSubmit={async (e) => {
313
- e.preventDefault();
314
- const fd = new FormData(e.currentTarget);
315
- await login(fd.get('email') as string, fd.get('password') as string);
316
- }}>
317
- <input name="email" type="email" />
318
- <input name="password" type="password" />
319
- {loginError && <p>{loginError.message}</p>}
320
- <button disabled={isLoggingIn}>Login</button>
321
- </form>
322
- );
323
- }
324
- ```
325
-
326
- Once logged in, authenticated hooks work automatically:
327
-
328
- ```tsx
329
- import { useOrders, useCustomer, useAddresses } from '@behio/storefront-sdk/react';
330
-
331
- const { data: orders } = useOrders();
332
- const { data: profile, updateProfile } = useCustomer();
333
- const { addresses, createAddress, deleteAddress } = useAddresses();
334
- ```
335
-
336
- → [Auto token refresh](#auto-token-refresh)
337
-
338
- <a id="b-server-components--server-actions"></a>
339
- ## 5. Server Components & Server Actions (secure path)
340
-
341
- For the **most secure** integration — API key stays on the server, tokens in httpOnly cookies, forms work without JavaScript (progressive enhancement).
342
-
343
- **Server Component — data fetching:**
344
-
345
- ```tsx
346
- // app/products/page.tsx (server component — no "use client")
347
- import { BehioStorefront } from '@behio/storefront-sdk';
348
-
349
- export default async function ProductsPage() {
350
- const shop = new BehioStorefront({
351
- apiKey: process.env.BEHIO_API_KEY!, // ← private, server-only
352
- });
353
-
354
- const products = await shop.catalog.getProducts({ limit: 24 });
355
-
356
- return (
357
- <div>
358
- {products.items.map(p => (
359
- <a key={p.id} href={`/products/${p.slug}`}>
360
- <h3>{p.name}</h3>
361
- <p>{p.price.amount} {p.price.currency}</p>
362
- </a>
363
- ))}
364
- </div>
365
- );
366
- }
367
- ```
368
-
369
- **Server Actions — mutations:**
370
-
371
- ```tsx
372
- // app/actions.ts
373
- 'use server';
374
- import { BehioStorefront } from '@behio/storefront-sdk';
375
- import { cookies } from 'next/headers';
376
-
377
- function getShop() {
378
- return new BehioStorefront({
379
- apiKey: process.env.BEHIO_API_KEY!,
380
- });
381
- }
382
-
383
- export async function loginAction(_: unknown, formData: FormData) {
384
- const shop = getShop();
385
- try {
386
- const result = await shop.auth.login({
387
- email: formData.get('email') as string,
388
- password: formData.get('password') as string,
389
- });
390
-
391
- const cookieStore = await cookies();
392
- cookieStore.set('behio_access', result.accessToken, {
393
- httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900,
394
- });
395
- cookieStore.set('behio_refresh', result.refreshToken, {
396
- httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 86400,
397
- });
398
-
399
- return { success: true, error: null };
400
- } catch (err) {
401
- return { success: false, error: err instanceof Error ? err.message : 'Login failed' };
402
- }
403
- }
404
-
405
- export async function addToCartAction(productId: string, quantity = 1) {
406
- const shop = getShop();
407
- const cookieStore = await cookies();
408
- const cartSession = cookieStore.get('behio_cart_session')?.value;
409
- if (cartSession) shop.setCartSession(cartSession);
410
-
411
- const result = await shop.cart.addItem({ productId, quantity });
412
-
413
- if (result.newSessionToken) {
414
- cookieStore.set('behio_cart_session', result.newSessionToken, {
415
- sameSite: 'lax', maxAge: 30 * 86400,
416
- });
417
- }
418
- return result;
419
- }
420
- ```
421
-
422
- ```tsx
423
- // app/auth/page.tsx
424
- 'use client';
425
- import { useActionState } from 'react';
426
- import { loginAction } from '../actions';
427
-
428
- export default function LoginPage() {
429
- const [state, formAction, isPending] = useActionState(loginAction, { success: false, error: null });
430
-
431
- return (
432
- <form action={formAction}>
433
- <input name="email" type="email" placeholder="Email" required />
434
- <input name="password" type="password" placeholder="Password" required />
435
- {state.error && <p>{state.error}</p>}
436
- <button disabled={isPending}>{isPending ? 'Logging in...' : 'Login'}</button>
437
- </form>
438
- );
439
- }
440
- ```
441
-
442
- ---
443
-
444
- # 🛠 Vanilla JS / Node.js path
445
-
446
- For Node.js backends, serverless functions, CLI tools, Deno, Bun, or any non-React JavaScript environment.
447
-
448
- ## 1. Create a client
449
-
450
- ```typescript
451
- import { BehioStorefront } from '@behio/storefront-sdk';
452
-
453
- const shop = new BehioStorefront({
454
- apiKey: 'pk_live_xxx',
455
- locale: 'cs', // optional
456
- currency: 'CZK', // optional
457
- timeout: 30000, // optional — request timeout (ms)
458
- retries: 1, // optional — retry failed requests
459
- });
460
- ```
461
-
462
- ## 2. Call any endpoint
463
-
464
- Every API method returns a Promise. Types are fully auto-completed.
465
-
466
- ```typescript
467
- // Catalog
468
- const products = await shop.catalog.getProducts({
469
- limit: 20,
470
- sort: 'newest',
471
- categories: ['electronics', 'books'], // array filters sent as repeated query params
472
- hasDiscount: true,
25
+ const storefront = new BehioStorefront({
26
+ apiKey: 'pk_live_your_key',
473
27
  });
474
- const product = await shop.catalog.getProduct('my-product-slug');
475
- const { categories } = await shop.catalog.getCategories();
476
28
 
477
- // Auth
478
- const tokens = await shop.auth.register({ email: '...', password: '...' });
479
- await shop.auth.login({ email: '...', password: '...' });
29
+ // Fetch products
30
+ const { items } = await storefront.catalog.getProducts({ limit: 12 });
480
31
 
481
- // Cart
482
- await shop.cart.addItem({ productId: 'xxx', quantity: 2 });
483
- const cart = await shop.cart.get();
484
- await shop.cart.applyDiscount('SAVE20');
32
+ // Add to cart
33
+ await storefront.cart.addItem({ productId: items[0].id, quantity: 1 });
485
34
 
486
35
  // Checkout
487
- const order = await shop.checkout.createOrder({
36
+ const order = await storefront.checkout.createOrder({
488
37
  email: 'customer@example.com',
489
- shippingAddress: { /* ... */ },
490
- billingAddress: { /* ... */ },
491
- });
492
-
493
- // Orders (auth required)
494
- const orders = await shop.orders.list();
495
- const detail = await shop.orders.get('ORD-123');
496
-
497
- // Public order tracking
498
- const tracked = await shop.orders.track('tracking-token-uuid');
499
- ```
500
-
501
- → [All API methods](#all-api-methods)
502
-
503
- ---
504
-
505
- # 📖 Reference
506
-
507
- ## All React hooks
508
-
509
- | Hook | Purpose | Auth required |
510
- |------|---------|---------------|
511
- | [`useShopInfo()`](#useshopinfo) | E-shop info (name, currencies, languages) | No |
512
- | [`useShopSeo(opts?)`](#useshopseo) | Per-locale SEO metadata for the homepage | No |
513
- | [`useProducts(query?)`](#useproducts) | Product list with filters, pagination, search | No |
514
- | [`useProduct(slug)`](#useproduct) | Product detail | No |
515
- | [`useCategories(locale?)`](#usecategories) | Category tree | No |
516
- | [`useLabels(locale?)`](#uselabels) | Product labels/tags | No |
517
- | [`useFeatured()`](#usefeatured) | Featured products | No |
518
- | [`useFilters()`](#usefilters) | Dynamic filter fields | No |
519
- | [`useSearch(query)`](#usesearch) | Debounced product search | No |
520
- | [`useCart()`](#usecart) | Cart state + actions | No |
521
- | [`useCartCount()`](#usecartcount) | Cart item count (lightweight) | No |
522
- | [`useAuth()`](#useauth) | Login, register, logout | No |
523
- | [`useCheckout()`](#usecheckout) | Create order | No* |
524
- | [`useOrders(opts?)`](#useorders) | Order list | Yes |
525
- | [`useOrder(num)`](#useorder) | Order detail + cancel | Yes |
526
- | [`useCustomer()`](#usecustomer) | Profile + update | Yes |
527
- | [`useAddresses()`](#useaddresses) | Address CRUD | Yes |
528
- | [`usePages()` / `usePage(slug)`](#usepages) | CMS pages | No |
529
- | [`useBundles()` / `useBundle(slug)`](#usebundles) | Active bundles (sets) with auto-computed savings | No |
530
- | [`useCrossSell(productSlug)`](#usecrosssell) | Related / upsell / cross-sell products per product | No |
531
- | [`useProductPromotions(productSlug)`](#useproductpromotions) | Active promotions applicable to a product (with countdown) | No |
532
-
533
- \* Guest checkout works without auth if the e-shop allows it.
534
-
535
- ### useShopInfo
536
- ```typescript
537
- const { data, isLoading, error } = useShopInfo();
538
- // data: ShopInfo
539
- ```
540
-
541
- ### useShopSeo
542
-
543
- Returns per-locale SEO metadata (title, description, keywords, OG tags). Falls back to the shop's default locale when `locale` is omitted, and to `metaTitle / metaDescription` on the shop itself if there's no per-locale override.
544
-
545
- ```tsx
546
- const { data: seo } = useShopSeo({ locale: 'cs' });
547
- // data: { locale, title, description, keywords, ogTitle, ogDescription, ogImage }
548
- ```
549
-
550
- **Server-side / SSR** — use the core client directly to fetch at request time and pass as `initialData`:
551
-
552
- ```tsx
553
- // app/[locale]/page.tsx (Next.js RSC)
554
- import { BehioStorefront } from '@behio/storefront-sdk';
555
- import type { Metadata } from 'next';
556
-
557
- const shop = new BehioStorefront({ apiKey: process.env.BEHIO_API_KEY! });
558
-
559
- export async function generateMetadata({ params }: { params: { locale: string } }): Promise<Metadata> {
560
- const seo = await shop.getShopSeo(params.locale);
561
- return {
562
- title: seo.title ?? undefined,
563
- description: seo.description ?? undefined,
564
- openGraph: {
565
- title: seo.ogTitle ?? seo.title ?? undefined,
566
- description: seo.ogDescription ?? seo.description ?? undefined,
567
- images: seo.ogImage ? [seo.ogImage] : undefined,
568
- },
569
- };
570
- }
571
-
572
- export default async function Home({ params }: { params: { locale: string } }) {
573
- const seo = await shop.getShopSeo(params.locale);
574
- // Pass to client component for React Query hydration:
575
- // <ClientHome initialSeo={seo} />
576
- }
577
- ```
578
-
579
- Client hydration:
580
- ```tsx
581
- 'use client';
582
- import { useShopSeo } from '@behio/storefront-sdk/react';
583
-
584
- export function ClientHome({ initialSeo }: { initialSeo: ShopSeo }) {
585
- const { data } = useShopSeo({ locale: 'cs', initialData: initialSeo });
586
- return <h1>{data.title}</h1>;
587
- }
588
- ```
589
-
590
- ### useProducts
591
-
592
- Supports **both** traditional pagination and infinite scroll from one hook.
593
-
594
- ```typescript
595
- const {
596
- items, // flat array (all pages merged) — for infinite scroll
597
- data, // PaginatedResponse (current page) — for traditional pagination
598
- total, totalPages, currentPage, limit,
599
-
600
- // Traditional pagination
601
- page, setPage,
602
-
603
- // Infinite scroll
604
- loadMore, hasMore, isLoadingMore,
605
-
606
- // State
607
- isLoading, isFetching, error, isError, refetch,
608
- } = useProducts({
609
- page: 1,
610
- limit: 24,
611
-
612
- // Scalar filters
613
- category: 'electronics', // single category slug
614
- label: 'new', // single label slug
615
- priceMin: 100,
616
- priceMax: 5000,
617
- currency: 'CZK',
618
- locale: 'cs',
619
- sort: 'price_asc', // or ProductSort.PRICE_ASC
620
- inStock: true,
621
- search: 'keyboard',
622
- customFields: { material: 'aluminum' },
623
-
624
- // Array filters — sent as repeated query params (?ids=a&ids=b)
625
- ids: ['prod_1', 'prod_2'], // filter to specific IDs
626
- slugs: ['red-shoes', 'blue-hat'], // filter to specific slugs
627
- categories: ['electronics', 'books'], // OR logic (in ANY of these)
628
- labels: ['sale', 'new'], // AND logic (must have ALL)
629
- excludeIds: ['prod_999'], // exclude specific IDs (e.g. related products)
630
- excludeCategories: ['archive'], // exclude products in these categories
631
-
632
- // Boolean / time filters
633
- hasDiscount: true, // only products with compareAtPrice
634
- isFeatured: true, // only featured products
635
- createdAfter: Date.now() - 7 * 864e5, // created in last 7 days (epoch ms)
636
-
637
- // Hook options
638
- enabled: true, // skip fetch until true
639
- initialData: prefetchedData, // SSR hydration
38
+ shippingAddress: { firstName: 'Jan', lastName: 'Novak', street: 'Vodickova 12', city: 'Praha', zip: '11000', country: 'CZ' },
640
39
  });
641
40
  ```
642
41
 
643
- **Infinite scroll example:**
644
- ```tsx
645
- const { items, loadMore, hasMore, isLoadingMore } = useProducts({ limit: 24 });
646
-
647
- return (
648
- <>
649
- {items.map(p => <ProductCard key={p.id} product={p} />)}
650
- {hasMore && <button onClick={loadMore} disabled={isLoadingMore}>Load more</button>}
651
- </>
652
- );
653
- ```
654
-
655
- **Traditional pagination example:**
656
- ```tsx
657
- const { data, page, setPage, totalPages } = useProducts({ limit: 24 });
658
-
659
- return (
660
- <>
661
- {data?.items.map(p => <ProductCard key={p.id} product={p} />)}
662
- <Pagination page={page} total={totalPages} onChange={setPage} />
663
- </>
664
- );
665
- ```
666
-
667
- ### useProduct
668
- ```typescript
669
- const { data, isLoading, error } = useProduct('product-slug', {
670
- locale: 'cs',
671
- currency: 'CZK',
672
- initialData: prefetchedData, // for SSR hydration
673
- });
674
- // data: ProductDetail
675
- ```
676
-
677
- ### useCategories
678
- ```typescript
679
- const { data, isLoading } = useCategories('cs');
680
- // data: Category[] (tree structure with children)
681
- ```
682
-
683
- ### useLabels
684
- ```typescript
685
- const { data } = useLabels('cs');
686
- // data: ProductLabel[]
687
- ```
688
-
689
- ### useFeatured
690
- ```typescript
691
- const { data } = useFeatured();
692
- // data: PaginatedResponse<ProductListItem>
693
- ```
694
-
695
- ### useFilters
696
- ```typescript
697
- const { data } = useFilters();
698
- // data: FilterField[] — use to build dynamic filter UI
699
- ```
700
-
701
- ### useSearch
702
- ```typescript
703
- const { data, isLoading } = useSearch(query, { debounceMs: 300, limit: 10 });
704
- // Debounced — only fetches after user stops typing
705
- ```
706
-
707
- ### useCart
708
- ```typescript
709
- const {
710
- cart, // Cart | null
711
- isLoading, error,
712
- isEmpty, // boolean
713
- itemCount, // number
714
- // Actions
715
- addItem, // (productId, quantity?) => Promise
716
- updateQuantity, // (itemId, quantity) => Promise
717
- removeItem, // (itemId) => Promise
718
- clear, // () => Promise
719
- applyDiscount, // (code) => Promise
720
- removeDiscount, // () => Promise
721
- merge, // () => Promise — merges anonymous cart after login
722
- // States
723
- isAdding, isUpdating, isRemoving,
724
- } = useCart();
725
- ```
726
-
727
- ### useCartCount
728
- ```typescript
729
- const count = useCartCount();
730
- // number — lightweight, reads from cache first
731
- ```
732
-
733
- ### useAuth
734
- ```typescript
735
- const {
736
- isLoggedIn, customer,
737
- login, // (email, password) => Promise
738
- register, // (input) => Promise
739
- logout, // () => Promise
740
- forgotPassword, // (email) => Promise
741
- resetPassword, // (token, newPassword) => Promise
742
- verifyEmail, // (token) => Promise
743
- isLoggingIn, isRegistering,
744
- loginError, registerError,
745
- } = useAuth();
746
- ```
747
-
748
- ### useCheckout
749
- ```typescript
750
- const {
751
- createOrder, // (input) => Promise<OrderDetail>
752
- isCreating, error,
753
- order, // OrderDetail | null — last created order
754
- reset, // () => void
755
- } = useCheckout();
756
- ```
757
-
758
- ### useOrders
759
- ```typescript
760
- const { data, isLoading } = useOrders({ page: 1, limit: 10 });
761
- // data: PaginatedResponse<OrderListItem>
762
- ```
763
-
764
- ### useOrder
765
- ```typescript
766
- const { data, cancel, isCancelling } = useOrder('ORD-123');
767
- // data: OrderDetail
768
- ```
769
-
770
- ### useCustomer
771
- ```typescript
772
- const { data, updateProfile, isUpdating } = useCustomer();
773
- // data: CustomerProfile
774
- ```
775
-
776
- ### useAddresses
777
- ```typescript
778
- const {
779
- addresses,
780
- createAddress, updateAddress, deleteAddress,
781
- isCreating, isDeleting,
782
- } = useAddresses();
783
- ```
784
-
785
- ### usePages
786
- ```typescript
787
- const { data: pages } = usePages('cs');
788
- const { data: page } = usePage('about-us', 'cs');
789
- ```
790
-
791
- ### useBundles / useBundle
792
-
793
- **What it is.** A "bundle" is a merchant-curated set of products sold together
794
- at a single fixed price that's (usually) lower than buying the components
795
- individually — think *starter kit*, *holiday gift set*, *3-for-2 deals*, or
796
- *"breakfast combo"*. The merchant defines what goes in (which products, what
797
- quantities, what cover image) and sets one total price for the whole thing.
798
-
799
- **Why it matters.** Bundles are one of the highest-ROI features in e-commerce:
800
- they raise average order value without having to discount individual products,
801
- they give customers a clear "good deal" signal (the savings badge), and they
802
- let you clear slow-moving inventory by pairing it with fast-movers. Most eshop
803
- platforms treat bundles as paid add-ons or plugins — here it's native.
804
-
805
- **Where you use it.**
806
-
807
- - **Homepage / landing pages** — list active bundles as hero cards with cover
808
- image + `-25%` badge. Drives impulse purchase.
809
- - **Category pages** — show a relevant bundle ("Everything for grilling") next
810
- to individual products in the same category.
811
- - **Cart / checkout** — suggest a bundle as upsell ("Add these two more items
812
- and get the whole set with a 20% discount").
813
- - **Dedicated `/bundles` page** — marketing landing for all active sets.
814
-
815
- **What's in the data:**
816
-
817
- - `bundlePrice` — what the customer pays for the whole set
818
- - `itemsSum` — what the components would cost individually (sum of default prices)
819
- - `savings` — `itemsSum - bundlePrice` (absolute savings in the currency)
820
- - `savingsPercent` — pre-computed percent so you don't have to do the math in JSX
821
- - `items[]` — the components with their quantities (for display and stock check)
822
- - `endsAt` — optional expiry timestamp; if set, the bundle auto-deactivates
42
+ ## React Hooks
823
43
 
824
44
  ```tsx
825
- import { useBundles, useBundle, useCart, useBehio } from '@behio/storefront-sdk/react';
826
-
827
- // ---- Homepage hero: all active bundles ----
828
- function BundlesGrid() {
829
- const { data, isLoading } = useBundles();
830
- if (isLoading) return <Skeleton />;
831
- if (!data?.items.length) return null; // no active bundles → hide section
45
+ import { BehioProvider, useProducts, useCart, useAddToCart } from '@behio/storefront-sdk/react';
832
46
 
47
+ function App() {
833
48
  return (
834
- <section className="grid grid-cols-3 gap-4">
835
- {data.items.map((bundle) => (
836
- <a key={bundle.id} href={`/bundle/${bundle.slug}`} className="relative">
837
- {bundle.coverImage && <img src={bundle.coverImage} alt={bundle.name} />}
838
- <h3>{bundle.name}</h3>
839
- <div>
840
- <strong>{bundle.bundlePrice} {bundle.currency}</strong>
841
- {bundle.savings > 0 && (
842
- <>
843
- <s>{bundle.itemsSum} {bundle.currency}</s>
844
- <span className="badge">-{bundle.savingsPercent}%</span>
845
- </>
846
- )}
847
- </div>
848
- <p>{bundle.items.length} products · save {bundle.savings} {bundle.currency}</p>
849
- </a>
850
- ))}
851
- </section>
852
- );
853
- }
854
-
855
- // ---- Bundle detail page with "Add to cart" ----
856
- function BundleDetail({ slug }: { slug: string }) {
857
- const { data: bundle, isLoading } = useBundle(slug);
858
- const { client } = useBehio();
859
- const { refresh } = useCart();
860
-
861
- if (isLoading) return <Skeleton />;
862
- if (!bundle) return <NotFound />;
863
-
864
- async function addToCart() {
865
- await client.cart.addBundle(bundle.id, 1);
866
- await refresh(); // cart badge updates everywhere
867
- }
868
-
869
- return (
870
- <article>
871
- <h1>{bundle.name}</h1>
872
- <p>{bundle.description}</p>
873
-
874
- <ul>
875
- {bundle.items.map((item) => (
876
- <li key={item.productId}>
877
- {item.quantity}× {item.name}
878
- {item.defaultPrice && (
879
- <span className="text-muted">
880
- ({item.defaultPrice} {bundle.currency} /pc regular price)
881
- </span>
882
- )}
883
- </li>
884
- ))}
885
- </ul>
886
-
887
- <div className="price-box">
888
- <strong>{bundle.bundlePrice} {bundle.currency}</strong>
889
- {bundle.savings > 0 && (
890
- <p>
891
- Individually it would cost <s>{bundle.itemsSum} {bundle.currency}</s> —
892
- you save <strong>{bundle.savings} {bundle.currency}</strong>
893
- ({bundle.savingsPercent}%)
894
- </p>
895
- )}
896
- <button onClick={addToCart}>Add the whole bundle to cart</button>
897
- </div>
898
- </article>
899
- );
900
- }
901
- ```
902
-
903
- **Behind the scenes at checkout.** When the customer buys a bundle, Behio
904
- splits it into individual order items with prices distributed proportionally
905
- based on each component's default price (this is kept so accounting and
906
- stock decrements work correctly). The invoice total matches `bundlePrice` —
907
- the customer sees one line item, inventory is decremented component-by-component.
908
-
909
- ---
910
-
911
- ### useCrossSell
912
-
913
- **What it is.** Three related lists of product recommendations shown on a
914
- product detail page:
915
-
916
- - **Related** — alternatives for the same need (another leash, another dog
917
- food). "If this one caught your eye, here are similar options in the same
918
- category."
919
- - **Upsell** — a better / more premium version. "Looking at the basic collar?
920
- Here's the leather-stitched premium version for 2× the price." Goal: raise
921
- average order value by steering to a better margin.
922
- - **Cross-sell** — complementary products. Leash for the collar, cleaner for
923
- the bowl. Pure AOV boost on PDP and in cart.
924
-
925
- **Why split them.** Each type has a different UX role and should be phrased
926
- differently. Mixed together they lose context — the customer can't tell why
927
- they're being shown.
928
-
929
- **Where to use it.**
930
-
931
- - **Product detail page** — three separate sections below the description
932
- (or in a sidebar column). Highest conversion impact is **Cross-sell
933
- "Frequently bought together"** right next to the "Add to cart" button.
934
- - **Cart sidebar** — mini cross-sell widget ("Don't forget these too").
935
- - **Post-purchase page** — "Want to add these?" for a follow-up order.
936
-
937
- **Note.** The endpoint returns **only active products from the same eshop**.
938
- If you linked a product in admin and later disabled or deleted it, it drops
939
- from the list automatically — no need to handle that on the frontend.
940
-
941
- ```tsx
942
- import { useCrossSell, useBehio } from '@behio/storefront-sdk/react';
943
-
944
- function CrossSellSection({ title, items }: { title: string; items: CrossSellItem[] }) {
945
- if (!items?.length) return null; // hide empty section
946
- return (
947
- <section>
948
- <h2>{title}</h2>
949
- <div className="carousel">
950
- {items.map((item) => (
951
- <a key={item.productId} href={`/product/${item.slug}`} className="card">
952
- {item.imageUrl && <img src={item.imageUrl} alt={item.name} />}
953
- <h4>{item.name}</h4>
954
- <span>{item.price}</span>
955
- {item.stockCached === 0 && <span className="text-red">Sold out</span>}
956
- </a>
957
- ))}
958
- </div>
959
- </section>
960
- );
961
- }
962
-
963
- function ProductDetail({ slug }: { slug: string }) {
964
- const { data: crossSell } = useCrossSell(slug);
965
-
966
- return (
967
- <>
968
- {/* ... product info ... */}
969
-
970
- <CrossSellSection
971
- title="Frequently bought together"
972
- items={crossSell?.crossSell ?? []}
973
- />
974
- <CrossSellSection
975
- title="You might also like"
976
- items={crossSell?.related ?? []}
977
- />
978
- <CrossSellSection
979
- title="Want a premium version?"
980
- items={crossSell?.upsell ?? []}
981
- />
982
- </>
49
+ <BehioProvider client={storefront}>
50
+ <ProductList />
51
+ </BehioProvider>
983
52
  );
984
53
  }
985
- ```
986
-
987
- **Tip — merge into a single section.** If you have sparse data and want
988
- to keep it simple, combine all three into one list:
989
-
990
- ```tsx
991
- const allRecommendations = [
992
- ...(crossSell?.crossSell ?? []),
993
- ...(crossSell?.related ?? []),
994
- ...(crossSell?.upsell ?? []),
995
- ].slice(0, 6);
996
- ```
997
-
998
- ---
999
-
1000
- ### useProductPromotions
1001
-
1002
- **What it is.** Returns the currently-active promotions (`Eshop_Promotion`)
1003
- applicable to a given product — i.e. all promotions where
1004
- `startsAt <= now < endsAt` and the product matches the promotion scope
1005
- (`ALL_PRODUCTS`, `SPECIFIC_PRODUCTS`, `CATEGORIES`, or `LABELS`; the backend
1006
- resolves it for you).
1007
-
1008
- **What it is NOT.** It's not a computation of the *final discounted price* —
1009
- that's a separate layer (the cart evaluator). This hook is purely for the
1010
- **display layer**: badges, countdowns, "sale price until midnight" banners.
1011
54
 
1012
- **Why it's separate.** The most conversion-effective marketing element in
1013
- e-commerce is **urgency + scarcity**. "Sale ends in 2h 14m 37s" right on the
1014
- PDP measurably lifts conversion — it's a well-established best practice
1015
- (Amazon Lightning Deals, Booking.com "3 people booked in the last 24h",
1016
- etc.). The hook delivers the data; you build the countdown component.
55
+ function ProductList() {
56
+ const { data, isLoading } = useProducts({ limit: 12 });
57
+ const addToCart = useAddToCart();
1017
58
 
1018
- **Where to use it.**
59
+ if (isLoading) return <div>Loading...</div>;
1019
60
 
1020
- - **Product card in a list** — a `-20%` badge or a "SALE" flag.
1021
- - **Product detail** — a large banner with countdown above the price: "Buy
1022
- before midnight to save $20."
1023
- - **Cart** a warning like "Your discount expires in 5 minutes" to nudge
1024
- checkout completion.
1025
-
1026
- **Fields returned:**
1027
-
1028
- - `id`, `name`, `slug` — promotion identity
1029
- - `type` — `FLASH_SALE` / `SEASONAL` / `CLEARANCE` / `BOGO` / `BUNDLE` / `LOYALTY`
1030
- - `discountType` — `PERCENTAGE` / `FIXED_AMOUNT` / `FREE_SHIPPING` / `BUY_X_GET_Y`
1031
- - `discountValue` — the value (% for PERCENTAGE, currency amount for FIXED_AMOUNT, …)
1032
- - `endsAt` — end timestamp. If `null`, the promotion runs indefinitely.
1033
- - `badgeText`, `badgeColor` — merchant-set badge text and color (e.g. "-30%"
1034
- in red). If both are `null`, fall back to a default derived from `discountType`.
1035
- - `showCountdown` — **respect this!** If the merchant opted out of showing
1036
- a countdown, don't render one. Not all promotions (CLEARANCE, LOYALTY)
1037
- make sense to count down.
1038
- - `couponRequired` — the promotion applies only after the customer enters a
1039
- code at checkout. On the PDP show an info box ("Use code SUMMER at
1040
- checkout"); don't present it as an automatic discount.
1041
-
1042
- ```tsx
1043
- import { useProductPromotions } from '@behio/storefront-sdk/react';
1044
- import { useEffect, useState } from 'react';
1045
-
1046
- function ProductPromotionBanner({ slug }: { slug: string }) {
1047
- // Refetch every 4 minutes so the promotion list updates when one ends
1048
- // and another begins. The countdown itself doesn't need a refetch —
1049
- // it ticks locally off `endsAt`.
1050
- const { data } = useProductPromotions(slug, { refetchIntervalMs: 4 * 60_000 });
1051
- const promotions = data?.items ?? [];
1052
-
1053
- if (!promotions.length) return null;
1054
-
1055
- // Highest-priority promotion (the BE already sorts by priority DESC,
1056
- // then createdAt ASC).
1057
- const promo = promotions[0];
1058
-
1059
- const badge = promo.badgeText ?? formatDefaultBadge(promo);
1060
- const badgeColor = promo.badgeColor ?? '#ef4444';
1061
-
1062
- return (
1063
- <div className="promo-banner" style={{ backgroundColor: badgeColor + '20', borderColor: badgeColor }}>
1064
- <div>
1065
- <span className="badge" style={{ backgroundColor: badgeColor }}>{badge}</span>
1066
- <strong>{promo.name}</strong>
1067
- {promo.couponRequired && (
1068
- <p>Use code <code>{promo.name}</code> at checkout to redeem</p>
1069
- )}
1070
- </div>
1071
- {promo.showCountdown && promo.endsAt && <Countdown endsAt={promo.endsAt} />}
61
+ return data.items.map(p => (
62
+ <div key={p.id}>
63
+ <h3>{p.name} {p.price} {p.currency}</h3>
64
+ <button onClick={() => addToCart.mutateAsync({ productId: p.id, quantity: 1 })}>
65
+ Add to Cart
66
+ </button>
1072
67
  </div>
1073
- );
1074
- }
1075
-
1076
- function Countdown({ endsAt }: { endsAt: number }) {
1077
- const [now, setNow] = useState(Date.now());
1078
- useEffect(() => {
1079
- const id = setInterval(() => setNow(Date.now()), 1000);
1080
- return () => clearInterval(id);
1081
- }, []);
1082
-
1083
- const ms = Math.max(0, endsAt - now);
1084
- if (ms === 0) return <span>Sale ended</span>;
1085
-
1086
- const d = Math.floor(ms / 86400_000);
1087
- const h = Math.floor((ms % 86400_000) / 3600_000);
1088
- const m = Math.floor((ms % 3600_000) / 60_000);
1089
- const s = Math.floor((ms % 60_000) / 1000);
1090
-
1091
- if (d > 0) return <span>Ends in {d}d {h}h {m}m</span>;
1092
- return <span>Ends in {h}h {m}m {s}s</span>;
1093
- }
1094
-
1095
- function formatDefaultBadge(p: { discountType: string; discountValue: number }) {
1096
- switch (p.discountType) {
1097
- case 'PERCENTAGE': return `-${p.discountValue}%`;
1098
- case 'FIXED_AMOUNT': return `-${p.discountValue}`;
1099
- case 'FREE_SHIPPING': return 'Free shipping';
1100
- case 'BUY_X_GET_Y': return `${p.discountValue}+1 FREE`;
1101
- default: return 'SALE';
1102
- }
68
+ ));
1103
69
  }
1104
70
  ```
1105
71
 
1106
- **Pattern product card badge.** If you only need a small flag on a
1107
- product card in a list (no countdown), call the hook per card and pick the
1108
- first promo:
72
+ ## What's Included
1109
73
 
1110
- ```tsx
1111
- function ProductCardBadge({ slug }: { slug: string }) {
1112
- const { data } = useProductPromotions(slug);
1113
- const promo = data?.items[0];
1114
- if (!promo) return null;
1115
- return (
1116
- <span className="badge" style={{ background: promo.badgeColor ?? '#ef4444' }}>
1117
- {promo.badgeText ?? formatDefaultBadge(promo)}
1118
- </span>
1119
- );
1120
- }
1121
- ```
74
+ ### SDK Modules
1122
75
 
1123
- Heads-up in list contexts this fires N requests (one per card). For
1124
- a 200-product page, either add a bulk endpoint of your own or keep the
1125
- requests cheap with a long React Query `staleTime`.
76
+ | Module | Description |
77
+ |--------|-------------|
78
+ | `catalog` | Products, categories, labels, search, filters, bundles, cross-sell, promotions |
79
+ | `auth` | Register, login, logout, password reset, token refresh |
80
+ | `cart` | Items, discounts, gift cards, bundles, cart merge |
81
+ | `checkout` | Create orders with atomic stock/payment validation |
82
+ | `orders` | List, detail, tracking, cancel |
83
+ | `customer` | Profile, addresses, password change |
84
+ | `wishlist` | Add, remove, check |
85
+ | `reviews` | Submit, list, vote helpful |
86
+ | `addresses` | Address autocomplete with debounce hook |
87
+ | `returns` | Submit return requests |
88
+ | `consent` | Cookie consent (GDPR) |
89
+ | `quotes` | B2B quote requests |
90
+ | `pages` | CMS pages |
1126
91
 
1127
- ## All API methods
92
+ ### React Hooks (30+)
1128
93
 
1129
- ### Catalog
1130
- ```typescript
1131
- shop.catalog.getProducts(query?) → PaginatedResponse<ProductListItem>
1132
- shop.catalog.getProduct(slug) → ProductDetail
1133
- shop.catalog.getCategories(locale?) → { categories: Category[] }
1134
- shop.catalog.getCategory(slug, locale?) → CategoryDetail
1135
- shop.catalog.getCategoryProducts(slug, q?) → PaginatedResponse<ProductListItem>
1136
- shop.catalog.getLabels(locale?) → { labels: ProductLabel[] }
1137
- shop.catalog.getFeatured() → PaginatedResponse<ProductListItem>
1138
- shop.catalog.getFilters() → { filters: FilterField[] }
1139
- shop.catalog.search(query, opts?) → PaginatedResponse<ProductListItem>
1140
- shop.catalog.getBundles() → { items: Bundle[] }
1141
- shop.catalog.getBundle(slug) → Bundle
1142
- shop.catalog.getCrossSell(productSlug) → { related, upsell, crossSell: CrossSellItem[] }
1143
- shop.catalog.getProductPromotions(slug) → { items: ActivePromotion[] }
1144
- ```
94
+ `useProducts` · `useProduct` · `useCategories` · `useCategoryProducts` · `useFeaturedProducts` · `useLabels` · `useProductSearch` · `useFilters` · `useBundles` · `useBundle` · `useCrossSell` · `useProductPromotions` · `useGiftCardBalance` · `useCart` · `useAddToCart` · `useUpdateCartItem` · `useRemoveCartItem` · `useCheckout` · `useOrders` · `useOrder` · `useOrderTracking` · `useCustomerProfile` · `useAddresses` · `useAddressAutocomplete` · `useWishlist` · `useProductReviews` · `useSubmitReview` · `useShopInfo` · `useShopSeo` · `useCartCount`
1145
95
 
1146
- ### Cart — bundles
1147
- ```typescript
1148
- shop.cart.addBundle(bundleId, quantity?) → Cart
1149
- shop.cart.updateBundleQuantity(id, qty) → Cart
1150
- shop.cart.removeBundle(bundleId) → Cart
1151
- ```
1152
-
1153
- ### Auth
1154
- ```typescript
1155
- shop.auth.register(input) → AuthTokens
1156
- shop.auth.login(input) → AuthTokens
1157
- shop.auth.refresh(token?) → AuthTokens
1158
- shop.auth.logout(token?) → MessageResponse
1159
- shop.auth.forgotPassword(email) → MessageResponse
1160
- shop.auth.resetPassword(token, password) → MessageResponse
1161
- shop.auth.verifyEmail(token) → MessageResponse
1162
- shop.auth.isLoggedIn() → boolean
1163
- ```
96
+ ### Framework Support
1164
97
 
1165
- ### Cart
1166
- ```typescript
1167
- shop.cart.get() → Cart
1168
- shop.cart.addItem({ productId, quantity }) → Cart & { newSessionToken? }
1169
- shop.cart.updateQuantity(itemId, qty) → Cart
1170
- shop.cart.removeItem(itemId) → Cart
1171
- shop.cart.clear() → void
1172
- shop.cart.merge() → Cart
1173
- shop.cart.applyDiscount(code) → Cart
1174
- shop.cart.removeDiscount() → Cart
1175
- ```
98
+ - **Next.js** — Server components + client hooks, SSR ready
99
+ - **React + Vite** — Standard SPA setup
100
+ - **Nuxt 3** — Composables with SSR
101
+ - **Vue + Vite** Provide/inject pattern
102
+ - **Vanilla JS** — Works in any runtime (Node.js, Deno, Bun, Cloudflare Workers)
1176
103
 
1177
- ### Checkout
1178
- ```typescript
1179
- shop.checkout.createOrder(input) → OrderDetail
1180
- ```
104
+ ### Built-in Features
1181
105
 
1182
- ### Orders
1183
- ```typescript
1184
- shop.orders.list(opts?) → PaginatedResponse<OrderListItem>
1185
- shop.orders.get(orderNumber) → OrderDetail
1186
- shop.orders.cancel(orderNumber) → OrderDetail
1187
- shop.orders.track(trackingToken) → OrderDetail // no auth needed
1188
- ```
106
+ - Automatic JWT token refresh on 401
107
+ - Configurable retry with backoff (5xx, 429)
108
+ - Rate limit tracking and warnings
109
+ - Request/response interceptors
110
+ - Event system (auth, cart, order lifecycle)
111
+ - Cart session persistence
1189
112
 
1190
- ### Customer
1191
- ```typescript
1192
- shop.customer.getProfile() → CustomerProfile
1193
- shop.customer.updateProfile(data) → CustomerProfile
1194
- shop.customer.changePassword(current, new) → MessageResponse
1195
- shop.customer.getAddresses() → { items: CustomerAddress[] }
1196
- shop.customer.createAddress(data) → CustomerAddress
1197
- shop.customer.updateAddress(id, data) → CustomerAddress
1198
- shop.customer.deleteAddress(id) → void
1199
- ```
1200
-
1201
- ### Pages
1202
- ```typescript
1203
- shop.pages.list(locale?) → { pages: Page[] }
1204
- shop.pages.get(slug, locale?) → PageDetail
1205
- ```
1206
-
1207
- ### Instance utilities
1208
- ```typescript
1209
- shop.getShopInfo() → ShopInfo
1210
- shop.getShopSeo(locale?) → ShopSeo (per-locale SEO for SSR/metadata)
1211
- shop.setTokens({ accessToken, refreshToken })
1212
- shop.clearTokens()
1213
- shop.getAccessToken() → string | undefined
1214
- shop.getRefreshToken() → string | undefined
1215
- shop.setCartSession(token)
1216
- shop.getCartSession() → string | undefined
1217
- shop.clearCartSession()
1218
- shop.on(event, handler) → unsubscribe fn
1219
- shop.addRequestInterceptor(fn) → unsubscribe fn
1220
- shop.addResponseInterceptor(fn) → unsubscribe fn
1221
- shop.getRateLimitInfo() → { remaining, reset }
1222
- ```
113
+ ## Documentation
1223
114
 
1224
- ## Types
115
+ Full API reference, framework guides, and examples:
1225
116
 
1226
- ```typescript
1227
- ProductListItem {
1228
- id, slug, name, shortDescription?, sku, gtin?,
1229
- price: ProductPrice,
1230
- inStock, stockQuantity?, image?,
1231
- labels: ProductLabel[], isFeatured
1232
- }
1233
-
1234
- ProductDetail extends ProductListItem {
1235
- longDescription?,
1236
- images[], categories[], variants[],
1237
- volumePricing[], customFields, seo,
1238
- weight?, weightUnit?,
1239
- }
1240
-
1241
- ProductPrice { amount: number, currency: string, compareAtPrice?: number | null }
1242
-
1243
- Cart {
1244
- id, sessionToken?,
1245
- items: CartItem[],
1246
- subtotal, discountTotal, discount?,
1247
- grandTotal, currency, itemCount
1248
- }
1249
-
1250
- CartItem {
1251
- id, product: CartItemProduct,
1252
- quantity, unitPrice, totalPrice,
1253
- priceChanged, volumePriceApplied
1254
- }
1255
-
1256
- OrderDetail {
1257
- orderNumber, status, paymentStatus, fulfillmentStatus,
1258
- items[], shippingAddress, billingAddress,
1259
- subtotal, taxTotal, shippingTotal, discountTotal, grandTotal, currency,
1260
- statusHistory[], trackingToken?
1261
- }
1262
-
1263
- CustomerProfile {
1264
- id, email, firstName?, lastName?, phone?, emailVerified
1265
- }
1266
- ```
1267
-
1268
- ## Constants / Enums
1269
-
1270
- Type-safe constants for common values — autocomplete, iterable, zero runtime cost.
1271
-
1272
- ```typescript
1273
- import {
1274
- ProductSort,
1275
- OrderStatuses,
1276
- PaymentStatuses,
1277
- FulfillmentStatuses,
1278
- AddressTypes,
1279
- } from '@behio/storefront-sdk';
1280
-
1281
- // Use in queries
1282
- shop.catalog.getProducts({ sort: ProductSort.PRICE_ASC });
1283
-
1284
- // Type-safe comparisons
1285
- if (order.status === OrderStatuses.SHIPPED) { /* ... */ }
1286
- if (order.paymentStatus === PaymentStatuses.PAID) { /* ... */ }
1287
-
1288
- // Iterate for dropdowns
1289
- Object.values(ProductSort); // ['price_asc', 'price_desc', ...]
1290
- Object.values(OrderStatuses); // ['PENDING', 'CONFIRMED', ...]
1291
- ```
1292
-
1293
- Available constants:
1294
- - `ProductSort`: `PRICE_ASC`, `PRICE_DESC`, `NAME_ASC`, `NAME_DESC`, `NEWEST`, `FEATURED`
1295
- - `OrderStatuses`: `PENDING`, `CONFIRMED`, `PROCESSING`, `SHIPPED`, `DELIVERED`, `CANCELLED`, `REFUNDED`
1296
- - `PaymentStatuses`: `UNPAID`, `PAID`, `PARTIALLY_REFUNDED`, `REFUNDED`
1297
- - `FulfillmentStatuses`: `UNFULFILLED`, `PARTIALLY_FULFILLED`, `FULFILLED`
1298
- - `AddressTypes`: `SHIPPING`, `BILLING`
1299
-
1300
- ## Error handling
1301
-
1302
- ```typescript
1303
- import { BehioApiError, BehioNetworkError } from '@behio/storefront-sdk';
1304
-
1305
- try {
1306
- await shop.auth.login({ email: 'wrong', password: 'bad' });
1307
- } catch (err) {
1308
- if (err instanceof BehioApiError) {
1309
- err.status; // HTTP status (401)
1310
- err.code; // Typed error code ('INVALID_CREDENTIALS')
1311
- err.message; // Human-readable message
1312
- err.body; // Full API response
1313
- err.isRetryable; // boolean
1314
- err.is('INVALID_CREDENTIALS'); // typed helper
1315
- }
1316
- if (err instanceof BehioNetworkError) {
1317
- err.code; // 'NETWORK_ERROR' | 'TIMEOUT'
1318
- err.isRetryable; // always true
1319
- }
1320
- }
1321
- ```
1322
-
1323
- **Error codes:** `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `VALIDATION_ERROR`, `CONFLICT`, `RATE_LIMITED`, `CART_EMPTY`, `PRODUCT_NOT_FOUND`, `INVALID_CREDENTIALS`, `INVALID_DISCOUNT`, `DISCOUNT_EXPIRED`, `TOKEN_EXPIRED`, `TOKEN_INVALID`, `EMAIL_ALREADY_EXISTS`, `ORDER_NOT_CANCELLABLE`, `INTERNAL_ERROR`, `NETWORK_ERROR`, `TIMEOUT`, `UNKNOWN`
1324
-
1325
- ## Events & interceptors
1326
-
1327
- ```typescript
1328
- // Events
1329
- shop.on('auth:login', (data) => console.log('Logged in'));
1330
- shop.on('auth:logout', () => console.log('Logged out'));
1331
- shop.on('auth:token-refresh', () => console.log('Token refreshed'));
1332
- shop.on('cart:updated', (cart) => console.log('Cart changed'));
1333
- shop.on('order:created', (order) => console.log('Order placed'));
1334
- shop.on('error', (err) => console.error('API error', err));
1335
- shop.on('rate-limit-warning', ({ remaining }) => console.warn(`${remaining} left`));
1336
-
1337
- // Unsubscribe
1338
- const unsub = shop.on('error', handler);
1339
- unsub();
1340
-
1341
- // Request interceptor — modify outgoing requests
1342
- shop.addRequestInterceptor((config) => {
1343
- config.headers['X-Custom'] = 'value';
1344
- return config;
1345
- });
1346
-
1347
- // Response interceptor — observe all responses
1348
- shop.addResponseInterceptor((res) => {
1349
- console.log(`${res.status}`);
1350
- });
1351
- ```
1352
-
1353
- Event types: `auth:login`, `auth:logout`, `auth:token-refresh`, `auth:token-refresh-failed`, `cart:updated`, `cart:cleared`, `order:created`, `error`, `request`, `response`, `rate-limit-warning`
1354
-
1355
- ## Auto token refresh
1356
-
1357
- When any authenticated request returns 401, the SDK automatically:
1358
- 1. Calls `auth.refresh()` with the stored refresh token
1359
- 2. Retries the original request with the new access token
1360
- 3. If refresh also fails → clears tokens, throws original error
1361
-
1362
- No code needed. A concurrent-request lock prevents multiple simultaneous refreshes.
1363
-
1364
- ## Cart session management
1365
-
1366
- Anonymous users get a `sessionToken` automatically on first `addItem`. It's stored in cookies/localStorage via `BehioProvider` (or manually via `shop.setCartSession()`).
1367
-
1368
- After login, call `shop.cart.merge()` (or use `useCart().merge()`) to combine the anonymous cart with the customer account. The React `useAuth()` hook does this automatically.
1369
-
1370
- ---
1371
-
1372
- ## React Query keys (for manual cache invalidation)
1373
-
1374
- ```typescript
1375
- ['behio', 'shop-info']
1376
- ['behio', 'products', queryString]
1377
- ['behio', 'product', slug]
1378
- ['behio', 'categories', locale?]
1379
- ['behio', 'labels', locale?]
1380
- ['behio', 'featured']
1381
- ['behio', 'filters']
1382
- ['behio', 'search', query]
1383
- ['behio', 'cart']
1384
- ['behio', 'customer']
1385
- ['behio', 'addresses']
1386
- ['behio', 'orders', params]
1387
- ['behio', 'order', orderNumber]
1388
- ['behio', 'pages', locale?]
1389
- ['behio', 'page', slug, locale?]
1390
- ```
117
+ **[sdk.behio.com](https://sdk.behio.com)**
1391
118
 
1392
119
  ## License
1393
120