@behio/storefront-sdk 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/README.md +606 -682
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -14,343 +14,114 @@ This SDK is the fastest way to connect your Next.js, React, or any JavaScript ap
14
14
  - **Scale-ready.** Redis caching, rate limiting, webhooks, API keys with scoped permissions. From MVP to millions of orders on the same stack.
15
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
16
 
17
- **Perfect for:**
18
- - Agencies building custom e-shops for clients
19
- - Brands that want their storefront to look and feel unique
20
- - SaaS apps that need built-in commerce
21
- - Headless migrations from Shopify, WooCommerce, PrestaShop
22
-
23
- ## Get started
24
-
25
- ```bash
26
- npm install @behio/storefront-sdk
27
- ```
28
-
29
- ## Two entry points
30
-
31
- ```typescript
32
- // Core — works everywhere (Node.js, browser, Deno, Bun, edge)
33
- import { BehioStorefront } from '@behio/storefront-sdk';
34
-
35
- // React — hooks + provider for Next.js / React apps
36
- import { BehioProvider, useProducts, useCart, useAuth } from '@behio/storefront-sdk/react';
37
- ```
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.
38
18
 
39
19
  ---
40
20
 
41
- ## Core SDK
21
+ ## 🚀 Get started
42
22
 
43
- ### Setup
23
+ Pick your integration path:
44
24
 
45
- ```typescript
46
- import { BehioStorefront } from '@behio/storefront-sdk';
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.
47
27
 
48
- const shop = new BehioStorefront({
49
- apiKey: 'pk_live_xxx', // Your API key this identifies your e-shop
50
- baseUrl: 'https://api.behio.com', // Optional, defaults to https://api.behio.com
51
- locale: 'cs', // Optional default locale
52
- currency: 'CZK', // Optional default currency
53
- timeout: 30000, // Optional request timeout (ms)
54
- retries: 1, // Optional retries on network/5xx errors
55
- });
56
- ```
57
-
58
- ### Catalog
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.
59
30
 
60
- ```typescript
61
- // List products with filters
62
- const products = await shop.catalog.getProducts({
63
- page: 1,
64
- limit: 24,
65
- category: 'electronics',
66
- label: 'new',
67
- priceMin: 100,
68
- priceMax: 5000,
69
- sort: 'price_asc', // price_asc | price_desc | name_asc | name_desc | newest | featured
70
- inStock: true,
71
- search: 'keyboard',
72
- customFields: { material: 'aluminum', weight_min: 500 },
73
- });
74
- // → { items: ProductListItem[], total, page, limit, totalPages }
75
-
76
- // Product detail
77
- const product = await shop.catalog.getProduct('mechanical-keyboard-rgb');
78
- // → { id, slug, name, price, variants, volumePricing, images, categories, labels, seo, ... }
79
-
80
- // Categories (tree structure)
81
- const { categories } = await shop.catalog.getCategories('cs');
82
-
83
- // Labels
84
- const { labels } = await shop.catalog.getLabels();
85
-
86
- // Featured products
87
- const featured = await shop.catalog.getFeatured();
88
-
89
- // Search
90
- const results = await shop.catalog.search('gaming mouse', { limit: 10 });
91
-
92
- // Available filters (for dynamic filter UI)
93
- const { filters } = await shop.catalog.getFilters();
94
- // → [{ key: 'material', name: 'Material', type: 'TEXT', values: ['aluminum', 'plastic'] }]
95
- ```
96
-
97
- ### Auth
98
-
99
- ```typescript
100
- // Register
101
- const tokens = await shop.auth.register({
102
- email: 'customer@example.com',
103
- password: 'SecurePass123!',
104
- firstName: 'Jan',
105
- lastName: 'Novak',
106
- });
107
- // → { accessToken: 'eyJ...', refreshToken: 'uuid' }
108
- // Tokens are automatically stored on the client instance
109
-
110
- // Login
111
- await shop.auth.login({ email: 'customer@example.com', password: 'SecurePass123!' });
112
-
113
- // Check if logged in
114
- shop.auth.isLoggedIn(); // true
115
-
116
- // Refresh token (rotates — old token invalidated)
117
- await shop.auth.refresh();
118
-
119
- // Logout
120
- await shop.auth.logout();
121
-
122
- // Password reset flow
123
- await shop.auth.forgotPassword('customer@example.com');
124
- await shop.auth.resetPassword('reset-token-from-email', 'NewPassword456!');
125
-
126
- // Email verification
127
- await shop.auth.verifyEmail('verification-token-from-email');
128
-
129
- // Manual token management (e.g. restore from localStorage)
130
- shop.setTokens({ accessToken: '...', refreshToken: '...' });
131
- shop.clearTokens();
132
- ```
133
-
134
- ### Cart
135
-
136
- ```typescript
137
- // Add item (creates session automatically for anonymous users)
138
- const cart = await shop.cart.addItem({ productId: 'product-id', quantity: 2 });
139
- // Cart session token is auto-saved internally
140
-
141
- // Get cart
142
- const cart = await shop.cart.get();
143
- // → { items: [...], subtotal, discountTotal, grandTotal, currency, itemCount }
144
-
145
- // Update quantity
146
- await shop.cart.updateQuantity('cart-item-id', 5);
147
-
148
- // Remove item
149
- await shop.cart.removeItem('cart-item-id');
150
-
151
- // Discount codes
152
- await shop.cart.applyDiscount('SAVE20');
153
- await shop.cart.removeDiscount();
154
-
155
- // Merge anonymous cart after login
156
- await shop.auth.login({ email: '...', password: '...' });
157
- await shop.cart.merge();
158
-
159
- // Clear cart
160
- await shop.cart.clear();
161
- ```
162
-
163
- ### Checkout
164
-
165
- ```typescript
166
- const order = await shop.checkout.createOrder({
167
- email: 'customer@example.com',
168
- phone: '+420123456789',
169
- customerNote: 'Gift wrap please',
170
- shippingAddress: {
171
- firstName: 'Jan',
172
- lastName: 'Novak',
173
- street: 'Hlavni 123',
174
- city: 'Praha',
175
- zip: '11000',
176
- country: 'CZ',
177
- },
178
- billingAddress: {
179
- firstName: 'Jan',
180
- lastName: 'Novak',
181
- street: 'Hlavni 123',
182
- city: 'Praha',
183
- zip: '11000',
184
- country: 'CZ',
185
- },
186
- });
187
- // → { orderNumber: 'ORD-123', status: 'PENDING', items, grandTotal, trackingToken, ... }
188
- ```
189
-
190
- ### Orders
191
-
192
- ```typescript
193
- // List customer orders (requires auth)
194
- const orders = await shop.orders.list({ page: 1, limit: 10 });
31
+ ---
195
32
 
196
- // Order detail
197
- const order = await shop.orders.get('ORD-123');
33
+ ## 📚 Table of contents
198
34
 
199
- // Cancel (only PENDING orders)
200
- await shop.orders.cancel('ORD-123');
35
+ **Setup**
36
+ - [Install](#install)
37
+ - [Get your API key](#get-your-api-key)
201
38
 
202
- // Track by token (no auth needed — shareable link)
203
- const order = await shop.orders.track('tracking-token-uuid');
204
- ```
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)
205
42
 
206
- ### Customer Profile
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)
207
52
 
208
- ```typescript
209
- const profile = await shop.customer.getProfile();
210
- await shop.customer.updateProfile({ firstName: 'Jana', phone: '+420987654321' });
211
- await shop.customer.changePassword('oldPassword', 'newPassword');
212
-
213
- // Addresses
214
- const { items: addresses } = await shop.customer.getAddresses();
215
- await shop.customer.createAddress({
216
- type: 'SHIPPING', isDefault: true,
217
- firstName: 'Jan', lastName: 'Novak',
218
- street: 'Hlavni 1', city: 'Praha', zip: '11000', country: 'CZ',
219
- });
220
- await shop.customer.updateAddress('address-id', { street: 'Nova 42' });
221
- await shop.customer.deleteAddress('address-id');
222
- ```
53
+ ---
223
54
 
224
- ### CMS Pages
55
+ ## Install
225
56
 
226
- ```typescript
227
- const { pages } = await shop.pages.list('cs');
228
- const about = await shop.pages.get('about-us', 'cs');
229
- // { slug, title, content (JSON), seoTitle, seoDescription }
57
+ ```bash
58
+ npm install @behio/storefront-sdk
59
+ # or
60
+ yarn add @behio/storefront-sdk
61
+ # or
62
+ pnpm add @behio/storefront-sdk
230
63
  ```
231
64
 
232
- ### Error Handling
65
+ The SDK has two entry points:
233
66
 
234
67
  ```typescript
235
- import { BehioApiError, BehioNetworkError } from '@behio/storefront-sdk';
68
+ // Core — works everywhere (Node.js, browser, Deno, Bun, edge runtimes)
69
+ import { BehioStorefront } from '@behio/storefront-sdk';
236
70
 
237
- try {
238
- await shop.auth.login({ email: 'wrong', password: 'bad' });
239
- } catch (err) {
240
- if (err instanceof BehioApiError) {
241
- err.status; // 401
242
- err.code; // 'INVALID_CREDENTIALS'
243
- err.message; // "Invalid email or password"
244
- err.isRetryable; // false
245
- err.is('INVALID_CREDENTIALS'); // true
246
- }
247
- if (err instanceof BehioNetworkError) {
248
- err.code; // 'TIMEOUT' | 'NETWORK_ERROR'
249
- err.isRetryable; // true
250
- }
251
- }
71
+ // React — hooks + provider for Next.js / React apps
72
+ import { BehioProvider, useProducts, useCart, useAuth } from '@behio/storefront-sdk/react';
252
73
  ```
253
74
 
254
- Error codes: `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `VALIDATION_ERROR`, `RATE_LIMITED`, `CART_EMPTY`, `PRODUCT_NOT_FOUND`, `INVALID_CREDENTIALS`, `INVALID_DISCOUNT`, `DISCOUNT_EXPIRED`, `TOKEN_EXPIRED`, `EMAIL_ALREADY_EXISTS`, `ORDER_NOT_CANCELLABLE`, `INTERNAL_ERROR`, `NETWORK_ERROR`, `TIMEOUT`
255
-
256
- ### Constants
257
-
258
- Type-safe constants for enums — autocomplete, iterable, zero runtime cost:
75
+ ## Get your API key
259
76
 
260
- ```typescript
261
- import {
262
- ProductSort,
263
- OrderStatuses,
264
- PaymentStatuses,
265
- FulfillmentStatuses,
266
- AddressTypes,
267
- } from '@behio/storefront-sdk';
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
268
81
 
269
- // Product sorting
270
- shop.catalog.getProducts({ sort: ProductSort.PRICE_ASC });
271
- // ProductSort.PRICE_ASC | PRICE_DESC | NAME_ASC | NAME_DESC | NEWEST | FEATURED
82
+ The API key identifies your e-shop. No shop domain needed.
272
83
 
273
- // Order status checks
274
- if (order.status === OrderStatuses.SHIPPED) { ... }
275
- // OrderStatuses.PENDING | CONFIRMED | PROCESSING | SHIPPED | DELIVERED | CANCELLED | REFUNDED
84
+ ---
276
85
 
277
- // Payment status
278
- if (order.paymentStatus === PaymentStatuses.PAID) { ... }
86
+ # 🎯 Next.js / React path
279
87
 
280
- // Fulfillment
281
- if (order.fulfillmentStatus === FulfillmentStatuses.FULFILLED) { ... }
88
+ You can build your Next.js storefront in **two ways** — pick one or mix them:
282
89
 
283
- // Address types
284
- shop.customer.createAddress({ type: AddressTypes.SHIPPING, ... });
90
+ ### [A. Client-side hooks](#1-setup-the-provider) 🪝
91
+ `BehioProvider` + hooks like `useProducts`, `useCart`, `useAuth`. Fast interactivity, automatic state management, optimistic updates.
285
92
 
286
- // Iterate for dropdowns/selects
287
- Object.values(ProductSort); // ['price_asc', 'price_desc', ...]
288
- Object.values(OrderStatuses); // ['PENDING', 'CONFIRMED', ...]
289
- ```
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.
290
95
 
291
- ### Events
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.
292
98
 
293
- ```typescript
294
- // Listen to SDK events
295
- shop.on('auth:login', (data) => console.log('Logged in'));
296
- shop.on('auth:logout', () => console.log('Logged out'));
297
- shop.on('auth:token-refresh', () => console.log('Token refreshed'));
298
- shop.on('cart:updated', (cart) => console.log('Cart changed'));
299
- shop.on('order:created', (order) => console.log('Order placed'));
300
- shop.on('error', (err) => console.error('API error', err));
301
- shop.on('rate-limit-warning', ({ remaining }) => console.warn(`Rate limit: ${remaining} left`));
99
+ **Best for:** SEO-critical pages, checkout, auth forms, sensitive mutations.
100
+ **Upside:** More secure private API key, better perceived performance, progressive enhancement.
302
101
 
303
- // Unsubscribe
304
- const unsub = shop.on('error', handler);
305
- unsub(); // stop listening
306
- ```
102
+ ### Recommendation
307
103
 
308
- ### Interceptors
309
-
310
- ```typescript
311
- // Log every request
312
- shop.addRequestInterceptor((config) => {
313
- console.log(`${config.method} ${config.url}`);
314
- return config;
315
- });
316
-
317
- // Log every response
318
- shop.addResponseInterceptor((response) => {
319
- console.log(`Status ${response.status}`);
320
- });
321
-
322
- // Add custom headers
323
- shop.addRequestInterceptor((config) => {
324
- config.headers['X-Custom'] = 'value';
325
- return config;
326
- });
327
- ```
328
-
329
- ### Auto token refresh
330
-
331
- When an authenticated request gets 401, the SDK automatically:
332
- 1. Calls `auth.refresh()` with the stored refresh token
333
- 2. Retries the original request with the new access token
334
- 3. If refresh fails too → clears tokens, throws the original error
335
-
336
- No code needed — it just works.
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)
337
108
 
338
109
  ---
339
110
 
340
- ## React / Next.js SDK
111
+ ## 1. Setup the provider
341
112
 
342
- ### Setup (Next.js App Router)
113
+ Create a client wrapper for the provider:
343
114
 
344
115
  ```tsx
345
- // app/providers.tsx — client component wrapper
116
+ // app/providers.tsx
346
117
  'use client';
347
118
  import { BehioProvider } from '@behio/storefront-sdk/react';
348
119
 
349
120
  export function Providers({ children }: { children: React.ReactNode }) {
350
121
  return (
351
122
  <BehioProvider
352
- apiKey={process.env.NEXT_PUBLIC_BEHIO_API_KEY!}
353
- baseUrl="/api" // Next.js proxy to avoid CORS
123
+ apiKey={process.env.NEXT_PUBLIC_BEHIO_API_KEY!} // e.g. "pk_live_6d3f255..."
124
+ baseUrl={process.env.NEXT_PUBLIC_BEHIO_API_URL} // optional defaults to https://api.behio.com
354
125
  locale="cs"
355
126
  currency="CZK"
356
127
  storage="cookies" // "cookies" | "localStorage" | "memory"
@@ -361,8 +132,10 @@ export function Providers({ children }: { children: React.ReactNode }) {
361
132
  }
362
133
  ```
363
134
 
135
+ Use it in your root layout (stays as a server component):
136
+
364
137
  ```tsx
365
- // app/layout.tsx — server component (no "use client" needed)
138
+ // app/layout.tsx
366
139
  import { Providers } from './providers';
367
140
 
368
141
  export default function RootLayout({ children }: { children: React.ReactNode }) {
@@ -376,60 +149,78 @@ export default function RootLayout({ children }: { children: React.ReactNode })
376
149
  }
377
150
  ```
378
151
 
379
- Next.js rewrite in `next.config.ts`:
152
+ Set your env variables:
153
+
154
+ ```bash
155
+ # .env.local
156
+ NEXT_PUBLIC_BEHIO_API_KEY=pk_live_xxxxxxxxxxxx
157
+ NEXT_PUBLIC_BEHIO_API_URL=https://api.behio.com # production
158
+ # NEXT_PUBLIC_BEHIO_API_URL=http://localhost:7007 # local dev
159
+
160
+ # For server-side (Server Actions, RSC) — NEVER expose to client
161
+ BEHIO_API_KEY=sk_live_xxxxxxxxxxxx
162
+ BEHIO_API_URL=https://api.behio.com
163
+ ```
164
+
165
+ **Optional:** Hide the backend URL behind a proxy. Add a Next.js rewrite in `next.config.ts` and set `baseUrl="/api"`:
380
166
  ```typescript
381
167
  async rewrites() {
382
168
  return [{ source: '/api/:path*', destination: 'https://api.behio.com/:path*' }];
383
169
  }
384
170
  ```
385
171
 
386
- ### useProducts Product listing with filters
172
+ ## 2. Fetch data with hooks
173
+
174
+ All catalog data available via React hooks. Each hook returns `{ data, isLoading, error, refetch }`.
387
175
 
388
176
  ```tsx
389
177
  'use client';
390
- import { useProducts } from '@behio/storefront-sdk/react';
178
+ import { useShopInfo, useProducts, useProduct, useCategories, formatPrice } from '@behio/storefront-sdk/react';
391
179
 
392
- export function ProductGrid() {
393
- const { data, isLoading } = useProducts({
394
- limit: 24,
395
- category: 'electronics',
396
- sort: 'newest',
397
- });
398
-
399
- if (isLoading) return <div>Loading...</div>;
180
+ export function HomePage() {
181
+ const { data: shop } = useShopInfo();
182
+ const { data: products } = useProducts({ limit: 12, sort: 'newest' });
183
+ const { data: cats } = useCategories();
400
184
 
401
185
  return (
402
- <div className="grid grid-cols-4 gap-4">
403
- {data?.items.map(product => (
404
- <div key={product.id}>
405
- <h3>{product.name}</h3>
406
- <p>{product.price.amount} {product.price.currency}</p>
407
- </div>
408
- ))}
186
+ <div>
187
+ <h1>{shop?.name}</h1>
188
+ <nav>
189
+ {cats?.map(c => <a key={c.id} href={`/category/${c.slug}`}>{c.name}</a>)}
190
+ </nav>
191
+ <div className="grid grid-cols-4 gap-4">
192
+ {products?.items.map(p => (
193
+ <div key={p.id}>
194
+ <h3>{p.name}</h3>
195
+ <p>{formatPrice(p.price.amount, p.price.currency)}</p>
196
+ </div>
197
+ ))}
198
+ </div>
409
199
  </div>
410
200
  );
411
201
  }
412
202
  ```
413
203
 
414
- ### useProduct Product detail with SSR prefetch
204
+ **SSR prefetch** (App Router server components):
415
205
 
416
206
  ```tsx
417
- // Server Component — prefetch data
207
+ // app/products/[slug]/page.tsx (server component)
418
208
  import { BehioStorefront } from '@behio/storefront-sdk';
419
209
  import { ProductDetail } from './ProductDetail';
420
210
 
421
- export default async function ProductPage({ params }: { params: { slug: string } }) {
211
+ export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
212
+ const { slug } = await params;
422
213
  const shop = new BehioStorefront({
423
214
  apiKey: process.env.BEHIO_API_KEY!,
424
215
  baseUrl: process.env.BEHIO_API_URL,
425
216
  });
426
- const initialData = await shop.catalog.getProduct(params.slug);
427
- return <ProductDetail slug={params.slug} initialData={initialData} />;
217
+ const initialData = await shop.catalog.getProduct(slug);
218
+ return <ProductDetail slug={slug} initialData={initialData} />;
428
219
  }
429
220
  ```
430
221
 
431
222
  ```tsx
432
- // Client Component — hydrates with SSR data, then syncs
223
+ // ProductDetail.tsx (client component — hydrates with SSR data)
433
224
  'use client';
434
225
  import { useProduct } from '@behio/storefront-sdk/react';
435
226
 
@@ -439,29 +230,35 @@ export function ProductDetail({ slug, initialData }) {
439
230
  }
440
231
  ```
441
232
 
442
- ### useCart Full cart management
233
+ [All available hooks](#all-react-hooks)
234
+
235
+ ## 3. Cart & checkout
236
+
237
+ `useCart` gives you everything — state, mutations, computed values, optimistic updates.
443
238
 
444
239
  ```tsx
445
240
  'use client';
446
- import { useCart, formatPrice } from '@behio/storefront-sdk/react';
241
+ import { useCart, useCartCount, formatPrice } from '@behio/storefront-sdk/react';
447
242
 
448
- export function Cart() {
243
+ export function CartButton() {
244
+ const count = useCartCount(); // lightweight — just the number
245
+ return <span>Cart ({count})</span>;
246
+ }
247
+
248
+ export function CartPage() {
449
249
  const {
450
250
  cart, isEmpty, itemCount,
451
251
  addItem, updateQuantity, removeItem, clear,
452
252
  applyDiscount, removeDiscount,
453
- isAdding, isUpdating,
454
253
  } = useCart();
455
254
 
456
255
  if (isEmpty) return <p>Your cart is empty</p>;
457
256
 
458
257
  return (
459
258
  <div>
460
- <h2>Cart ({itemCount})</h2>
461
259
  {cart?.items.map(item => (
462
260
  <div key={item.id}>
463
261
  <span>{item.product.name}</span>
464
- <span>{formatPrice(item.unitPrice, cart.currency)}</span>
465
262
  <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
466
263
  <span>{item.quantity}</span>
467
264
  <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
@@ -474,19 +271,38 @@ export function Cart() {
474
271
  }
475
272
  ```
476
273
 
477
- ### useCartCount — Lightweight badge
274
+ Checkout:
478
275
 
479
276
  ```tsx
480
277
  'use client';
481
- import { useCartCount } from '@behio/storefront-sdk/react';
278
+ import { useCheckout } from '@behio/storefront-sdk/react';
482
279
 
483
- export function NavCartBadge() {
484
- const count = useCartCount();
485
- return <span>Cart {count > 0 && `(${count})`}</span>;
280
+ export function CheckoutForm() {
281
+ const { createOrder, isCreating, order } = useCheckout();
282
+
283
+ if (order) return <h2>Order #{order.orderNumber} confirmed!</h2>;
284
+
285
+ return (
286
+ <form onSubmit={async (e) => {
287
+ e.preventDefault();
288
+ await createOrder({
289
+ email: 'customer@example.com',
290
+ shippingAddress: { firstName: 'Jan', lastName: 'Novak', street: 'Hlavni 1', city: 'Praha', zip: '11000', country: 'CZ' },
291
+ billingAddress: { firstName: 'Jan', lastName: 'Novak', street: 'Hlavni 1', city: 'Praha', zip: '11000', country: 'CZ' },
292
+ });
293
+ }}>
294
+ {/* form fields */}
295
+ <button disabled={isCreating}>{isCreating ? 'Processing...' : 'Place order'}</button>
296
+ </form>
297
+ );
486
298
  }
487
299
  ```
488
300
 
489
- ### useAuth Authentication
301
+ [Cart session management](#cart-session-management)
302
+
303
+ ## 4. Customer authentication
304
+
305
+ Register, login, logout — with automatic token persistence and anonymous cart merge.
490
306
 
491
307
  ```tsx
492
308
  'use client';
@@ -497,10 +313,10 @@ export function AuthSection() {
497
313
 
498
314
  if (isLoggedIn) {
499
315
  return (
500
- <div>
316
+ <>
501
317
  <p>Welcome, {customer?.firstName || customer?.email}</p>
502
318
  <button onClick={logout}>Logout</button>
503
- </div>
319
+ </>
504
320
  );
505
321
  }
506
322
 
@@ -510,63 +326,63 @@ export function AuthSection() {
510
326
  const fd = new FormData(e.currentTarget);
511
327
  await login(fd.get('email') as string, fd.get('password') as string);
512
328
  }}>
513
- <input name="email" type="email" placeholder="Email" />
514
- <input name="password" type="password" placeholder="Password" />
329
+ <input name="email" type="email" />
330
+ <input name="password" type="password" />
515
331
  {loginError && <p>{loginError.message}</p>}
516
- <button disabled={isLoggingIn}>{isLoggingIn ? 'Logging in...' : 'Login'}</button>
332
+ <button disabled={isLoggingIn}>Login</button>
517
333
  </form>
518
334
  );
519
335
  }
520
336
  ```
521
337
 
522
- ### useCheckout Order creation
338
+ Once logged in, authenticated hooks work automatically:
523
339
 
524
340
  ```tsx
525
- 'use client';
526
- import { useCheckout } from '@behio/storefront-sdk/react';
341
+ import { useOrders, useCustomer, useAddresses } from '@behio/storefront-sdk/react';
527
342
 
528
- export function CheckoutForm() {
529
- const { createOrder, isCreating, order, reset } = useCheckout();
343
+ const { data: orders } = useOrders();
344
+ const { data: profile, updateProfile } = useCustomer();
345
+ const { addresses, createAddress, deleteAddress } = useAddresses();
346
+ ```
530
347
 
531
- if (order) return <h2>Order #{order.orderNumber} confirmed!</h2>;
348
+ [Auto token refresh](#auto-token-refresh)
532
349
 
533
- return (
534
- <form onSubmit={async (e) => {
535
- e.preventDefault();
536
- await createOrder({ email: '...', shippingAddress: { ... }, billingAddress: { ... } });
537
- }}>
538
- {/* form fields */}
539
- <button disabled={isCreating}>{isCreating ? 'Processing...' : 'Place Order'}</button>
540
- </form>
541
- );
542
- }
543
- ```
350
+ <a id="b-server-components--server-actions"></a>
351
+ ## 5. Server Components & Server Actions (secure path)
544
352
 
545
- ### useSearchDebounced search
353
+ For the **most secure** integration API key stays on the server, tokens in httpOnly cookies, forms work without JavaScript (progressive enhancement).
354
+
355
+ **Server Component — data fetching:**
546
356
 
547
357
  ```tsx
548
- 'use client';
549
- import { useSearch } from '@behio/storefront-sdk/react';
550
- import { useState } from 'react';
358
+ // app/products/page.tsx (server component — no "use client")
359
+ import { BehioStorefront } from '@behio/storefront-sdk';
360
+
361
+ export default async function ProductsPage() {
362
+ const shop = new BehioStorefront({
363
+ apiKey: process.env.BEHIO_API_KEY!, // ← private, server-only
364
+ baseUrl: process.env.BEHIO_API_URL,
365
+ });
551
366
 
552
- function SearchBar() {
553
- const [query, setQuery] = useState('');
554
- const { data, isLoading } = useSearch(query, { debounceMs: 400, limit: 5 });
367
+ const products = await shop.catalog.getProducts({ limit: 24 });
555
368
 
556
369
  return (
557
370
  <div>
558
- <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
559
- {isLoading && <span>Searching...</span>}
560
- {data?.items.map(p => <div key={p.id}>{p.name}</div>)}
371
+ {products.items.map(p => (
372
+ <a key={p.id} href={`/products/${p.slug}`}>
373
+ <h3>{p.name}</h3>
374
+ <p>{p.price.amount} {p.price.currency}</p>
375
+ </a>
376
+ ))}
561
377
  </div>
562
378
  );
563
379
  }
564
380
  ```
565
381
 
566
- ### Server Actions + Forms
382
+ **Server Actions mutations:**
567
383
 
568
384
  ```tsx
569
- // app/actions.ts — server actions for checkout, auth, etc.
385
+ // app/actions.ts
570
386
  'use server';
571
387
  import { BehioStorefront } from '@behio/storefront-sdk';
572
388
  import { cookies } from 'next/headers';
@@ -578,23 +394,6 @@ function getShop() {
578
394
  });
579
395
  }
580
396
 
581
- export async function registerAction(formData: FormData) {
582
- const shop = getShop();
583
- const result = await shop.auth.register({
584
- email: formData.get('email') as string,
585
- password: formData.get('password') as string,
586
- firstName: formData.get('firstName') as string,
587
- lastName: formData.get('lastName') as string,
588
- });
589
-
590
- // Store tokens in httpOnly cookies (secure, server-side)
591
- const cookieStore = await cookies();
592
- cookieStore.set('behio_access', result.accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900 });
593
- cookieStore.set('behio_refresh', result.refreshToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 86400 });
594
-
595
- return { success: true };
596
- }
597
-
598
397
  export async function loginAction(_: unknown, formData: FormData) {
599
398
  const shop = getShop();
600
399
  try {
@@ -604,8 +403,12 @@ export async function loginAction(_: unknown, formData: FormData) {
604
403
  });
605
404
 
606
405
  const cookieStore = await cookies();
607
- cookieStore.set('behio_access', result.accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900 });
608
- cookieStore.set('behio_refresh', result.refreshToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 86400 });
406
+ cookieStore.set('behio_access', result.accessToken, {
407
+ httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900,
408
+ });
409
+ cookieStore.set('behio_refresh', result.refreshToken, {
410
+ httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 86400,
411
+ });
609
412
 
610
413
  return { success: true, error: null };
611
414
  } catch (err) {
@@ -613,64 +416,25 @@ export async function loginAction(_: unknown, formData: FormData) {
613
416
  }
614
417
  }
615
418
 
616
- export async function checkoutAction(formData: FormData) {
617
- const shop = getShop();
618
-
619
- // Restore auth from cookies
620
- const cookieStore = await cookies();
621
- const accessToken = cookieStore.get('behio_access')?.value;
622
- const cartSession = cookieStore.get('behio_cart_session')?.value;
623
- if (accessToken) shop.setTokens({ accessToken, refreshToken: '' });
624
- if (cartSession) shop.setCartSession(cartSession);
625
-
626
- const order = await shop.checkout.createOrder({
627
- email: formData.get('email') as string,
628
- phone: formData.get('phone') as string || undefined,
629
- customerNote: formData.get('note') as string || undefined,
630
- shippingAddress: {
631
- firstName: formData.get('shipping_firstName') as string,
632
- lastName: formData.get('shipping_lastName') as string,
633
- street: formData.get('shipping_street') as string,
634
- city: formData.get('shipping_city') as string,
635
- zip: formData.get('shipping_zip') as string,
636
- country: formData.get('shipping_country') as string,
637
- },
638
- billingAddress: {
639
- firstName: formData.get('billing_firstName') as string,
640
- lastName: formData.get('billing_lastName') as string,
641
- street: formData.get('billing_street') as string,
642
- city: formData.get('billing_city') as string,
643
- zip: formData.get('billing_zip') as string,
644
- country: formData.get('billing_country') as string,
645
- },
646
- });
647
-
648
- // Clear cart cookie
649
- cookieStore.delete('behio_cart_session');
650
-
651
- return { orderNumber: order.orderNumber, trackingToken: order.trackingToken };
652
- }
653
-
654
- export async function addToCartAction(productId: string, quantity: number = 1) {
419
+ export async function addToCartAction(productId: string, quantity = 1) {
655
420
  const shop = getShop();
656
-
657
421
  const cookieStore = await cookies();
658
422
  const cartSession = cookieStore.get('behio_cart_session')?.value;
659
423
  if (cartSession) shop.setCartSession(cartSession);
660
424
 
661
425
  const result = await shop.cart.addItem({ productId, quantity });
662
426
 
663
- // Save session token to cookie
664
427
  if (result.newSessionToken) {
665
- cookieStore.set('behio_cart_session', result.newSessionToken, { sameSite: 'lax', maxAge: 30 * 86400 });
428
+ cookieStore.set('behio_cart_session', result.newSessionToken, {
429
+ sameSite: 'lax', maxAge: 30 * 86400,
430
+ });
666
431
  }
667
-
668
432
  return result;
669
433
  }
670
434
  ```
671
435
 
672
436
  ```tsx
673
- // app/auth/page.tsx — Login form with useActionState
437
+ // app/auth/page.tsx
674
438
  'use client';
675
439
  import { useActionState } from 'react';
676
440
  import { loginAction } from '../actions';
@@ -678,297 +442,485 @@ import { loginAction } from '../actions';
678
442
  export default function LoginPage() {
679
443
  const [state, formAction, isPending] = useActionState(loginAction, { success: false, error: null });
680
444
 
681
- if (state.success) return <p>Logged in! Redirecting...</p>;
682
-
683
445
  return (
684
446
  <form action={formAction}>
685
447
  <input name="email" type="email" placeholder="Email" required />
686
448
  <input name="password" type="password" placeholder="Password" required />
687
- {state.error && <p className="text-red-500">{state.error}</p>}
449
+ {state.error && <p>{state.error}</p>}
688
450
  <button disabled={isPending}>{isPending ? 'Logging in...' : 'Login'}</button>
689
451
  </form>
690
452
  );
691
453
  }
692
454
  ```
693
455
 
694
- ```tsx
695
- // app/register/page.tsx — Register with progressive enhancement
696
- import { registerAction } from '../actions';
697
- import { redirect } from 'next/navigation';
698
-
699
- export default function RegisterPage() {
700
- async function handleRegister(formData: FormData) {
701
- 'use server';
702
- await registerAction(formData);
703
- redirect('/account');
704
- }
456
+ ---
705
457
 
706
- return (
707
- <form action={handleRegister}>
708
- <input name="firstName" placeholder="First name" />
709
- <input name="lastName" placeholder="Last name" />
710
- <input name="email" type="email" placeholder="Email" required />
711
- <input name="password" type="password" placeholder="Password" required />
712
- <button type="submit">Create Account</button>
713
- </form>
714
- );
715
- }
716
- ```
458
+ # 🛠 Vanilla JS / Node.js path
717
459
 
718
- ```tsx
719
- // app/products/[slug]/page.tsx — Add to cart with server action
720
- import { BehioStorefront } from '@behio/storefront-sdk';
721
- import { addToCartAction } from '../../actions';
722
- import { formatPrice } from '@behio/storefront-sdk/react';
460
+ For Node.js backends, serverless functions, CLI tools, Deno, Bun, or any non-React JavaScript environment.
723
461
 
724
- export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
725
- const { slug } = await params;
726
- const shop = new BehioStorefront({ apiKey: process.env.BEHIO_API_KEY!, baseUrl: process.env.BEHIO_API_URL });
727
- const product = await shop.catalog.getProduct(slug);
462
+ ## 1. Create a client
728
463
 
729
- return (
730
- <div>
731
- <h1>{product.name}</h1>
732
- <p>{product.longDescription}</p>
733
- <p className="text-2xl font-bold">{formatPrice(product.price.amount, product.price.currency)}</p>
734
-
735
- {product.price.compareAtPrice && (
736
- <p className="line-through text-gray-500">
737
- {formatPrice(product.price.compareAtPrice, product.price.currency)}
738
- </p>
739
- )}
740
-
741
- <form action={async () => {
742
- 'use server';
743
- await addToCartAction(product.id);
744
- }}>
745
- <button type="submit" disabled={!product.inStock}>
746
- {product.inStock ? 'Add to Cart' : 'Out of Stock'}
747
- </button>
748
- </form>
749
-
750
- {product.volumePricing.length > 0 && (
751
- <table>
752
- <thead><tr><th>Quantity</th><th>Price</th></tr></thead>
753
- <tbody>
754
- {product.volumePricing.map(vp => (
755
- <tr key={vp.minQuantity}>
756
- <td>{vp.minQuantity}+</td>
757
- <td>{formatPrice(vp.price, product.price.currency)}</td>
758
- </tr>
759
- ))}
760
- </tbody>
761
- </table>
762
- )}
763
- </div>
764
- );
765
- }
464
+ ```typescript
465
+ import { BehioStorefront } from '@behio/storefront-sdk';
466
+
467
+ const shop = new BehioStorefront({
468
+ apiKey: 'pk_live_xxx',
469
+ baseUrl: 'https://api.behio.com', // optional
470
+ locale: 'cs', // optional
471
+ currency: 'CZK', // optional
472
+ timeout: 30000, // optional — request timeout (ms)
473
+ retries: 1, // optional — retry failed requests
474
+ });
766
475
  ```
767
476
 
768
- ```tsx
769
- // app/checkout/page.tsx — Full checkout form with server action
770
- import { checkoutAction } from '../actions';
771
- import { redirect } from 'next/navigation';
772
-
773
- export default function CheckoutPage() {
774
- async function handleCheckout(formData: FormData) {
775
- 'use server';
776
- const result = await checkoutAction(formData);
777
- redirect(`/orders/confirmation?order=${result.orderNumber}`);
778
- }
477
+ ## 2. Call any endpoint
779
478
 
780
- return (
781
- <form action={handleCheckout}>
782
- <h2>Contact</h2>
783
- <input name="email" type="email" placeholder="Email" required />
784
- <input name="phone" type="tel" placeholder="Phone" />
785
-
786
- <h2>Shipping Address</h2>
787
- <input name="shipping_firstName" placeholder="First name" required />
788
- <input name="shipping_lastName" placeholder="Last name" required />
789
- <input name="shipping_street" placeholder="Street" required />
790
- <div style={{ display: 'flex', gap: 8 }}>
791
- <input name="shipping_city" placeholder="City" required />
792
- <input name="shipping_zip" placeholder="ZIP" required />
793
- </div>
794
- <input name="shipping_country" placeholder="Country code (CZ)" defaultValue="CZ" required />
795
-
796
- <h2>Billing Address</h2>
797
- <input name="billing_firstName" placeholder="First name" required />
798
- <input name="billing_lastName" placeholder="Last name" required />
799
- <input name="billing_street" placeholder="Street" required />
800
- <div style={{ display: 'flex', gap: 8 }}>
801
- <input name="billing_city" placeholder="City" required />
802
- <input name="billing_zip" placeholder="ZIP" required />
803
- </div>
804
- <input name="billing_country" placeholder="Country code (CZ)" defaultValue="CZ" required />
479
+ Every API method returns a Promise. Types are fully auto-completed.
805
480
 
806
- <textarea name="note" placeholder="Order note (optional)" />
481
+ ```typescript
482
+ // Catalog
483
+ const products = await shop.catalog.getProducts({ limit: 20, sort: 'newest' });
484
+ const product = await shop.catalog.getProduct('my-product-slug');
485
+ const { categories } = await shop.catalog.getCategories();
807
486
 
808
- <button type="submit">Place Order</button>
809
- </form>
810
- );
811
- }
487
+ // Auth
488
+ const tokens = await shop.auth.register({ email: '...', password: '...' });
489
+ await shop.auth.login({ email: '...', password: '...' });
490
+
491
+ // Cart
492
+ await shop.cart.addItem({ productId: 'xxx', quantity: 2 });
493
+ const cart = await shop.cart.get();
494
+ await shop.cart.applyDiscount('SAVE20');
495
+
496
+ // Checkout
497
+ const order = await shop.checkout.createOrder({
498
+ email: 'customer@example.com',
499
+ shippingAddress: { /* ... */ },
500
+ billingAddress: { /* ... */ },
501
+ });
502
+
503
+ // Orders (auth required)
504
+ const orders = await shop.orders.list();
505
+ const detail = await shop.orders.get('ORD-123');
506
+
507
+ // Public order tracking
508
+ const tracked = await shop.orders.track('tracking-token-uuid');
812
509
  ```
813
510
 
814
- ### All hooks
511
+ [All API methods](#all-api-methods)
815
512
 
816
- | Hook | Purpose | Auth |
817
- |------|---------|------|
818
- | `useShopInfo()` | E-shop info | No |
819
- | `useProducts(query?)` | Product list with filters | No |
820
- | `useProduct(slug)` | Product detail | No |
821
- | `useCategories(locale?)` | Category tree | No |
822
- | `useLabels(locale?)` | Labels/tags | No |
823
- | `useFeatured()` | Featured products | No |
824
- | `useFilters()` | Dynamic filter fields | No |
825
- | `useSearch(query)` | Debounced search | No |
826
- | `useCart()` | Cart state + actions | No |
827
- | `useCartCount()` | Item count (lightweight) | No |
828
- | `useAuth()` | Login, register, logout | No |
829
- | `useCheckout()` | Create order | No* |
830
- | `useOrders()` | Order list | Yes |
831
- | `useOrder(num)` | Order detail + cancel | Yes |
832
- | `useCustomer()` | Profile + update | Yes |
833
- | `useAddresses()` | Address CRUD | Yes |
834
- | `usePages()` / `usePage(slug)` | CMS pages | No |
513
+ ---
514
+
515
+ # 📖 Reference
516
+
517
+ ## All React hooks
518
+
519
+ | Hook | Purpose | Auth required |
520
+ |------|---------|---------------|
521
+ | [`useShopInfo()`](#useshopinfo) | E-shop info (name, currencies, languages) | No |
522
+ | [`useProducts(query?)`](#useproducts) | Product list with filters, pagination, search | No |
523
+ | [`useProduct(slug)`](#useproduct) | Product detail | No |
524
+ | [`useCategories(locale?)`](#usecategories) | Category tree | No |
525
+ | [`useLabels(locale?)`](#uselabels) | Product labels/tags | No |
526
+ | [`useFeatured()`](#usefeatured) | Featured products | No |
527
+ | [`useFilters()`](#usefilters) | Dynamic filter fields | No |
528
+ | [`useSearch(query)`](#usesearch) | Debounced product search | No |
529
+ | [`useCart()`](#usecart) | Cart state + actions | No |
530
+ | [`useCartCount()`](#usecartcount) | Cart item count (lightweight) | No |
531
+ | [`useAuth()`](#useauth) | Login, register, logout | No |
532
+ | [`useCheckout()`](#usecheckout) | Create order | No* |
533
+ | [`useOrders(opts?)`](#useorders) | Order list | Yes |
534
+ | [`useOrder(num)`](#useorder) | Order detail + cancel | Yes |
535
+ | [`useCustomer()`](#usecustomer) | Profile + update | Yes |
536
+ | [`useAddresses()`](#useaddresses) | Address CRUD | Yes |
537
+ | [`usePages()` / `usePage(slug)`](#usepages) | CMS pages | No |
538
+
539
+ \* Guest checkout works without auth if the e-shop allows it.
540
+
541
+ ### useShopInfo
542
+ ```typescript
543
+ const { data, isLoading, error } = useShopInfo();
544
+ // data: ShopInfo
545
+ ```
835
546
 
836
- \* Guest checkout works without auth if e-shop allows it.
547
+ ### useProducts
548
+ ```typescript
549
+ const { data, isLoading, error } = useProducts({
550
+ page: 1,
551
+ limit: 24,
552
+ category: 'electronics',
553
+ label: 'new',
554
+ priceMin: 100,
555
+ priceMax: 5000,
556
+ sort: 'price_asc',
557
+ inStock: true,
558
+ search: 'keyboard',
559
+ customFields: { material: 'aluminum' },
560
+ });
561
+ // data: PaginatedResponse<ProductListItem>
562
+ ```
837
563
 
838
- ### formatPrice
564
+ ### useProduct
565
+ ```typescript
566
+ const { data, isLoading, error } = useProduct('product-slug', {
567
+ locale: 'cs',
568
+ currency: 'CZK',
569
+ initialData: prefetchedData, // for SSR hydration
570
+ });
571
+ // data: ProductDetail
572
+ ```
839
573
 
574
+ ### useCategories
840
575
  ```typescript
841
- import { formatPrice } from '@behio/storefront-sdk/react';
576
+ const { data, isLoading } = useCategories('cs');
577
+ // data: Category[] (tree structure with children)
578
+ ```
842
579
 
843
- formatPrice(1499, 'CZK'); // "1 499,00 Kč"
844
- formatPrice(24.99, 'EUR'); // "24,99 €"
845
- formatPrice(1499, 'CZK', 'en'); // "CZK 1,499.00"
580
+ ### useLabels
581
+ ```typescript
582
+ const { data } = useLabels('cs');
583
+ // data: ProductLabel[]
846
584
  ```
847
585
 
848
- ---
586
+ ### useFeatured
587
+ ```typescript
588
+ const { data } = useFeatured();
589
+ // data: PaginatedResponse<ProductListItem>
590
+ ```
849
591
 
850
- ## How it works
592
+ ### useFilters
593
+ ```typescript
594
+ const { data } = useFilters();
595
+ // data: FilterField[] — use to build dynamic filter UI
596
+ ```
851
597
 
852
- - **Auth**: API key in `X-Api-Key` header identifies your e-shop. Customer JWT in `Authorization` header identifies the logged-in customer.
853
- - **Cart sessions**: Anonymous carts use `X-Cart-Session` header. Session token auto-managed by SDK.
854
- - **Token refresh**: 401 automatic refresh retry. Transparent to your code.
855
- - **Cart merge**: After login, anonymous cart automatically merged into customer cart.
856
- - **React Query**: All hooks use TanStack Query — caching, deduplication, background refetch.
857
- - **Optimistic updates**: Cart remove/update reflected instantly, rollback on error.
858
- - **SSR**: Pass `initialData` to hooks for server-prefetched data.
859
- - **Storage**: Auth tokens + cart session persisted to cookies/localStorage (configurable).
598
+ ### useSearch
599
+ ```typescript
600
+ const { data, isLoading } = useSearch(query, { debounceMs: 300, limit: 10 });
601
+ // Debounced only fetches after user stops typing
602
+ ```
860
603
 
861
- ---
604
+ ### useCart
605
+ ```typescript
606
+ const {
607
+ cart, // Cart | null
608
+ isLoading, error,
609
+ isEmpty, // boolean
610
+ itemCount, // number
611
+ // Actions
612
+ addItem, // (productId, quantity?) => Promise
613
+ updateQuantity, // (itemId, quantity) => Promise
614
+ removeItem, // (itemId) => Promise
615
+ clear, // () => Promise
616
+ applyDiscount, // (code) => Promise
617
+ removeDiscount, // () => Promise
618
+ merge, // () => Promise — merges anonymous cart after login
619
+ // States
620
+ isAdding, isUpdating, isRemoving,
621
+ } = useCart();
622
+ ```
623
+
624
+ ### useCartCount
625
+ ```typescript
626
+ const count = useCartCount();
627
+ // number — lightweight, reads from cache first
628
+ ```
862
629
 
863
- ## API Reference (for AI agents)
630
+ ### useAuth
631
+ ```typescript
632
+ const {
633
+ isLoggedIn, customer,
634
+ login, // (email, password) => Promise
635
+ register, // (input) => Promise
636
+ logout, // () => Promise
637
+ forgotPassword, // (email) => Promise
638
+ resetPassword, // (token, newPassword) => Promise
639
+ verifyEmail, // (token) => Promise
640
+ isLoggingIn, isRegistering,
641
+ loginError, registerError,
642
+ } = useAuth();
643
+ ```
644
+
645
+ ### useCheckout
646
+ ```typescript
647
+ const {
648
+ createOrder, // (input) => Promise<OrderDetail>
649
+ isCreating, error,
650
+ order, // OrderDetail | null — last created order
651
+ reset, // () => void
652
+ } = useCheckout();
653
+ ```
864
654
 
865
- ### Package exports
655
+ ### useOrders
656
+ ```typescript
657
+ const { data, isLoading } = useOrders({ page: 1, limit: 10 });
658
+ // data: PaginatedResponse<OrderListItem>
866
659
  ```
867
- @behio/storefront-sdk → BehioStorefront, types, BehioApiError, BehioNetworkError
868
- @behio/storefront-sdk/react → BehioProvider, all hooks, formatPrice
660
+
661
+ ### useOrder
662
+ ```typescript
663
+ const { data, cancel, isCancelling } = useOrder('ORD-123');
664
+ // data: OrderDetail
869
665
  ```
870
666
 
871
- ### Constructor
667
+ ### useCustomer
872
668
  ```typescript
873
- new BehioStorefront({ apiKey, baseUrl?, locale?, currency?, timeout?, retries?, retryDelay?, fetch? })
669
+ const { data, updateProfile, isUpdating } = useCustomer();
670
+ // data: CustomerProfile
874
671
  ```
875
672
 
876
- ### Instance methods
673
+ ### useAddresses
674
+ ```typescript
675
+ const {
676
+ addresses,
677
+ createAddress, updateAddress, deleteAddress,
678
+ isCreating, isDeleting,
679
+ } = useAddresses();
877
680
  ```
878
- shop.getShopInfo() → ShopInfo
879
- shop.setTokens({ accessToken, refreshToken }) / shop.clearTokens()
880
- shop.setCartSession(token) / shop.getCartSession() / shop.clearCartSession()
881
- shop.on(event, handler) unsubscribe fn
882
- shop.addRequestInterceptor(fn) unsubscribe fn
883
- shop.addResponseInterceptor(fn) → unsubscribe fn
884
- shop.getRateLimitInfo() → { remaining, reset }
681
+
682
+ ### usePages
683
+ ```typescript
684
+ const { data: pages } = usePages('cs');
685
+ const { data: page } = usePage('about-us', 'cs');
885
686
  ```
886
687
 
887
- ### Sub-modules
688
+ ## All API methods
689
+
690
+ ### Catalog
691
+ ```typescript
692
+ shop.catalog.getProducts(query?) → PaginatedResponse<ProductListItem>
693
+ shop.catalog.getProduct(slug) → ProductDetail
694
+ shop.catalog.getCategories(locale?) → { categories: Category[] }
695
+ shop.catalog.getCategory(slug, locale?) → CategoryDetail
696
+ shop.catalog.getCategoryProducts(slug, q?) → PaginatedResponse<ProductListItem>
697
+ shop.catalog.getLabels(locale?) → { labels: ProductLabel[] }
698
+ shop.catalog.getFeatured() → PaginatedResponse<ProductListItem>
699
+ shop.catalog.getFilters() → { filters: FilterField[] }
700
+ shop.catalog.search(query, opts?) → PaginatedResponse<ProductListItem>
888
701
  ```
889
- shop.catalog.getProducts(query?) → PaginatedResponse<ProductListItem>
890
- shop.catalog.getProduct(slug) → ProductDetail
891
- shop.catalog.getCategories(locale?) → { categories: Category[] }
892
- shop.catalog.getCategory(slug) → CategoryDetail
893
- shop.catalog.getCategoryProducts(slug, query?) → PaginatedResponse<ProductListItem>
894
- shop.catalog.getLabels(locale?) → { labels: ProductLabel[] }
895
- shop.catalog.getFeatured() → PaginatedResponse<ProductListItem>
896
- shop.catalog.getFilters() → { filters: FilterField[] }
897
- shop.catalog.search(query, opts?) → PaginatedResponse<ProductListItem>
898
702
 
899
- shop.auth.register(input) → AuthTokens
900
- shop.auth.login(input) → AuthTokens
901
- shop.auth.refresh(token?) → AuthTokens
902
- shop.auth.logout(token?) MessageResponse
903
- shop.auth.forgotPassword(email) MessageResponse
904
- shop.auth.resetPassword(token, password) → MessageResponse
905
- shop.auth.verifyEmail(token) → MessageResponse
906
- shop.auth.isLoggedIn() boolean
703
+ ### Auth
704
+ ```typescript
705
+ shop.auth.register(input) → AuthTokens
706
+ shop.auth.login(input) AuthTokens
707
+ shop.auth.refresh(token?) AuthTokens
708
+ shop.auth.logout(token?) → MessageResponse
709
+ shop.auth.forgotPassword(email) → MessageResponse
710
+ shop.auth.resetPassword(token, password) MessageResponse
711
+ shop.auth.verifyEmail(token) → MessageResponse
712
+ shop.auth.isLoggedIn() → boolean
713
+ ```
907
714
 
908
- shop.cart.get() Cart
909
- shop.cart.addItem({ productId, quantity }) → Cart & { newSessionToken? }
910
- shop.cart.updateQuantity(itemId, qty) → Cart
911
- shop.cart.removeItem(itemId) → Cart
912
- shop.cart.clear() void
913
- shop.cart.merge() → Cart
914
- shop.cart.applyDiscount(code) Cart
915
- shop.cart.removeDiscount() → Cart
715
+ ### Cart
716
+ ```typescript
717
+ shop.cart.get() → Cart
718
+ shop.cart.addItem({ productId, quantity }) → Cart & { newSessionToken? }
719
+ shop.cart.updateQuantity(itemId, qty) Cart
720
+ shop.cart.removeItem(itemId) → Cart
721
+ shop.cart.clear() void
722
+ shop.cart.merge() → Cart
723
+ shop.cart.applyDiscount(code) → Cart
724
+ shop.cart.removeDiscount() → Cart
725
+ ```
916
726
 
917
- shop.checkout.createOrder(input) → OrderDetail
727
+ ### Checkout
728
+ ```typescript
729
+ shop.checkout.createOrder(input) → OrderDetail
730
+ ```
918
731
 
919
- shop.orders.list(opts?) → PaginatedResponse<OrderListItem>
920
- shop.orders.get(orderNumber) → OrderDetail
921
- shop.orders.cancel(orderNumber) OrderDetail
922
- shop.orders.track(trackingToken) → OrderDetail
732
+ ### Orders
733
+ ```typescript
734
+ shop.orders.list(opts?) PaginatedResponse<OrderListItem>
735
+ shop.orders.get(orderNumber) → OrderDetail
736
+ shop.orders.cancel(orderNumber) → OrderDetail
737
+ shop.orders.track(trackingToken) → OrderDetail // no auth needed
738
+ ```
923
739
 
924
- shop.customer.getProfile() → CustomerProfile
925
- shop.customer.updateProfile(data) → CustomerProfile
926
- shop.customer.changePassword(current, new) MessageResponse
927
- shop.customer.getAddresses() { items: CustomerAddress[] }
928
- shop.customer.createAddress(data) CustomerAddress
929
- shop.customer.updateAddress(id, data) → CustomerAddress
930
- shop.customer.deleteAddress(id) void
740
+ ### Customer
741
+ ```typescript
742
+ shop.customer.getProfile() CustomerProfile
743
+ shop.customer.updateProfile(data) CustomerProfile
744
+ shop.customer.changePassword(current, new) MessageResponse
745
+ shop.customer.getAddresses() { items: CustomerAddress[] }
746
+ shop.customer.createAddress(data) CustomerAddress
747
+ shop.customer.updateAddress(id, data) → CustomerAddress
748
+ shop.customer.deleteAddress(id) → void
749
+ ```
931
750
 
932
- shop.pages.list(locale?) → { pages: Page[] }
933
- shop.pages.get(slug, locale?) → PageDetail
751
+ ### Pages
752
+ ```typescript
753
+ shop.pages.list(locale?) → { pages: Page[] }
754
+ shop.pages.get(slug, locale?) → PageDetail
934
755
  ```
935
756
 
936
- ### Key types
757
+ ### Instance utilities
758
+ ```typescript
759
+ shop.getShopInfo() → ShopInfo
760
+ shop.setTokens({ accessToken, refreshToken })
761
+ shop.clearTokens()
762
+ shop.getAccessToken() → string | undefined
763
+ shop.getRefreshToken() → string | undefined
764
+ shop.setCartSession(token)
765
+ shop.getCartSession() → string | undefined
766
+ shop.clearCartSession()
767
+ shop.on(event, handler) → unsubscribe fn
768
+ shop.addRequestInterceptor(fn) → unsubscribe fn
769
+ shop.addResponseInterceptor(fn) → unsubscribe fn
770
+ shop.getRateLimitInfo() → { remaining, reset }
771
+ ```
772
+
773
+ ## Types
774
+
937
775
  ```typescript
938
- ProductListItem { id, slug, name, shortDescription?, sku, gtin?, price: ProductPrice, inStock, stockQuantity?, image?, labels[], isFeatured }
939
- ProductDetail extends ProductListItem { longDescription?, images[], categories[], variants[], volumePricing[], customFields, seo, weight?, weightUnit? }
776
+ ProductListItem {
777
+ id, slug, name, shortDescription?, sku, gtin?,
778
+ price: ProductPrice,
779
+ inStock, stockQuantity?, image?,
780
+ labels: ProductLabel[], isFeatured
781
+ }
782
+
783
+ ProductDetail extends ProductListItem {
784
+ longDescription?,
785
+ images[], categories[], variants[],
786
+ volumePricing[], customFields, seo,
787
+ weight?, weightUnit?,
788
+ }
789
+
940
790
  ProductPrice { amount: number, currency: string, compareAtPrice?: number | null }
941
- Cart { id, sessionToken?, items: CartItem[], subtotal, discountTotal, discount?, grandTotal, currency, itemCount }
942
- CartItem { id, product: CartItemProduct, quantity, unitPrice, totalPrice, priceChanged, volumePriceApplied }
943
- OrderDetail { orderNumber, status, paymentStatus, fulfillmentStatus, items[], addresses, totals, statusHistory[], trackingToken? }
944
- CustomerProfile { id, email, firstName?, lastName?, phone?, emailVerified }
945
- BehioApiError { status, code: BehioErrorCode, body, message, isRetryable, is(code) }
946
- BehioNetworkError { code: 'NETWORK_ERROR' | 'TIMEOUT', isRetryable }
791
+
792
+ Cart {
793
+ id, sessionToken?,
794
+ items: CartItem[],
795
+ subtotal, discountTotal, discount?,
796
+ grandTotal, currency, itemCount
797
+ }
798
+
799
+ CartItem {
800
+ id, product: CartItemProduct,
801
+ quantity, unitPrice, totalPrice,
802
+ priceChanged, volumePriceApplied
803
+ }
804
+
805
+ OrderDetail {
806
+ orderNumber, status, paymentStatus, fulfillmentStatus,
807
+ items[], shippingAddress, billingAddress,
808
+ subtotal, taxTotal, shippingTotal, discountTotal, grandTotal, currency,
809
+ statusHistory[], trackingToken?
810
+ }
811
+
812
+ CustomerProfile {
813
+ id, email, firstName?, lastName?, phone?, emailVerified
814
+ }
947
815
  ```
948
816
 
949
- ### Const enums (runtime + type-safe)
817
+ ## Constants / Enums
818
+
819
+ Type-safe constants for common values — autocomplete, iterable, zero runtime cost.
820
+
950
821
  ```typescript
951
- ProductSort { PRICE_ASC, PRICE_DESC, NAME_ASC, NAME_DESC, NEWEST, FEATURED }
952
- OrderStatuses { PENDING, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED, REFUNDED }
953
- PaymentStatuses { UNPAID, PAID, PARTIALLY_REFUNDED, REFUNDED }
954
- FulfillmentStatuses { UNFULFILLED, PARTIALLY_FULFILLED, FULFILLED }
955
- AddressTypes { SHIPPING, BILLING }
822
+ import {
823
+ ProductSort,
824
+ OrderStatuses,
825
+ PaymentStatuses,
826
+ FulfillmentStatuses,
827
+ AddressTypes,
828
+ } from '@behio/storefront-sdk';
829
+
830
+ // Use in queries
831
+ shop.catalog.getProducts({ sort: ProductSort.PRICE_ASC });
832
+
833
+ // Type-safe comparisons
834
+ if (order.status === OrderStatuses.SHIPPED) { /* ... */ }
835
+ if (order.paymentStatus === PaymentStatuses.PAID) { /* ... */ }
836
+
837
+ // Iterate for dropdowns
838
+ Object.values(ProductSort); // ['price_asc', 'price_desc', ...]
839
+ Object.values(OrderStatuses); // ['PENDING', 'CONFIRMED', ...]
956
840
  ```
957
841
 
958
- ### React Provider props
842
+ Available constants:
843
+ - `ProductSort`: `PRICE_ASC`, `PRICE_DESC`, `NAME_ASC`, `NAME_DESC`, `NEWEST`, `FEATURED`
844
+ - `OrderStatuses`: `PENDING`, `CONFIRMED`, `PROCESSING`, `SHIPPED`, `DELIVERED`, `CANCELLED`, `REFUNDED`
845
+ - `PaymentStatuses`: `UNPAID`, `PAID`, `PARTIALLY_REFUNDED`, `REFUNDED`
846
+ - `FulfillmentStatuses`: `UNFULFILLED`, `PARTIALLY_FULFILLED`, `FULFILLED`
847
+ - `AddressTypes`: `SHIPPING`, `BILLING`
848
+
849
+ ## Error handling
850
+
959
851
  ```typescript
960
- BehioProviderProps {
961
- apiKey: string,
962
- baseUrl?: string,
963
- locale?: string,
964
- currency?: string,
965
- storage?: 'cookies' | 'localStorage' | 'memory' | StorageAdapter,
966
- children: ReactNode,
852
+ import { BehioApiError, BehioNetworkError } from '@behio/storefront-sdk';
853
+
854
+ try {
855
+ await shop.auth.login({ email: 'wrong', password: 'bad' });
856
+ } catch (err) {
857
+ if (err instanceof BehioApiError) {
858
+ err.status; // HTTP status (401)
859
+ err.code; // Typed error code ('INVALID_CREDENTIALS')
860
+ err.message; // Human-readable message
861
+ err.body; // Full API response
862
+ err.isRetryable; // boolean
863
+ err.is('INVALID_CREDENTIALS'); // typed helper
864
+ }
865
+ if (err instanceof BehioNetworkError) {
866
+ err.code; // 'NETWORK_ERROR' | 'TIMEOUT'
867
+ err.isRetryable; // always true
868
+ }
967
869
  }
968
870
  ```
969
871
 
970
- ### React Query keys
872
+ **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`
873
+
874
+ ## Events & interceptors
875
+
876
+ ```typescript
877
+ // Events
878
+ shop.on('auth:login', (data) => console.log('Logged in'));
879
+ shop.on('auth:logout', () => console.log('Logged out'));
880
+ shop.on('auth:token-refresh', () => console.log('Token refreshed'));
881
+ shop.on('cart:updated', (cart) => console.log('Cart changed'));
882
+ shop.on('order:created', (order) => console.log('Order placed'));
883
+ shop.on('error', (err) => console.error('API error', err));
884
+ shop.on('rate-limit-warning', ({ remaining }) => console.warn(`${remaining} left`));
885
+
886
+ // Unsubscribe
887
+ const unsub = shop.on('error', handler);
888
+ unsub();
889
+
890
+ // Request interceptor — modify outgoing requests
891
+ shop.addRequestInterceptor((config) => {
892
+ config.headers['X-Custom'] = 'value';
893
+ return config;
894
+ });
895
+
896
+ // Response interceptor — observe all responses
897
+ shop.addResponseInterceptor((res) => {
898
+ console.log(`${res.status}`);
899
+ });
971
900
  ```
901
+
902
+ 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`
903
+
904
+ ## Auto token refresh
905
+
906
+ When any authenticated request returns 401, the SDK automatically:
907
+ 1. Calls `auth.refresh()` with the stored refresh token
908
+ 2. Retries the original request with the new access token
909
+ 3. If refresh also fails → clears tokens, throws original error
910
+
911
+ No code needed. A concurrent-request lock prevents multiple simultaneous refreshes.
912
+
913
+ ## Cart session management
914
+
915
+ Anonymous users get a `sessionToken` automatically on first `addItem`. It's stored in cookies/localStorage via `BehioProvider` (or manually via `shop.setCartSession()`).
916
+
917
+ 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.
918
+
919
+ ---
920
+
921
+ ## React Query keys (for manual cache invalidation)
922
+
923
+ ```typescript
972
924
  ['behio', 'shop-info']
973
925
  ['behio', 'products', queryString]
974
926
  ['behio', 'product', slug]
@@ -986,34 +938,6 @@ BehioProviderProps {
986
938
  ['behio', 'page', slug, locale?]
987
939
  ```
988
940
 
989
- ### Event types
990
- ```
991
- 'auth:login' | 'auth:logout' | 'auth:token-refresh' | 'auth:token-refresh-failed'
992
- 'cart:updated' | 'cart:cleared' | 'order:created'
993
- 'error' | 'request' | 'response' | 'rate-limit-warning'
994
- ```
995
-
996
- ---
997
-
998
- ## Next.js full example
999
-
1000
- ```
1001
- app/
1002
- ├── layout.tsx ← BehioProvider
1003
- ├── page.tsx ← useShopInfo + useFeatured
1004
- ├── products/
1005
- │ ├── page.tsx ← useProducts
1006
- │ └── [slug]/page.tsx ← useProduct (SSR prefetch)
1007
- ├── cart/page.tsx ← useCart
1008
- ├── checkout/page.tsx ← useCheckout
1009
- ├── auth/page.tsx ← useAuth
1010
- ├── account/
1011
- │ ├── page.tsx ← useCustomer
1012
- │ ├── orders/page.tsx ← useOrders
1013
- │ └── addresses/page.tsx ← useAddresses
1014
- └── [slug]/page.tsx ← usePage (CMS)
1015
- ```
1016
-
1017
941
  ## License
1018
942
 
1019
943
  MIT