@behio/storefront-sdk 0.1.3 → 0.1.5

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
@@ -14,342 +14,113 @@ 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
42
-
43
- ### Setup
44
-
45
- ```typescript
46
- import { BehioStorefront } from '@behio/storefront-sdk';
47
-
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
59
-
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 });
21
+ ## 🚀 Get started
91
22
 
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
23
+ Pick your integration path:
109
24
 
110
- // Login
111
- await shop.auth.login({ email: 'customer@example.com', password: 'SecurePass123!' });
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.
112
27
 
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
- ```
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.
133
30
 
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
31
+ ---
191
32
 
192
- ```typescript
193
- // List customer orders (requires auth)
194
- const orders = await shop.orders.list({ page: 1, limit: 10 });
33
+ ## 📚 Table of contents
195
34
 
196
- // Order detail
197
- const order = await shop.orders.get('ORD-123');
35
+ **Setup**
36
+ - [Install](#install)
37
+ - [Get your API key](#get-your-api-key)
198
38
 
199
- // Cancel (only PENDING orders)
200
- await shop.orders.cancel('ORD-123');
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)
201
42
 
202
- // Track by token (no auth needed — shareable link)
203
- const order = await shop.orders.track('tracking-token-uuid');
204
- ```
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)
205
52
 
206
- ### Customer Profile
207
-
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`
75
+ ## Get your API key
255
76
 
256
- ### Constants
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
257
81
 
258
- Type-safe constants for enums autocomplete, iterable, zero runtime cost:
82
+ The API key identifies your e-shop. No shop domain needed.
259
83
 
260
- ```typescript
261
- import {
262
- ProductSort,
263
- OrderStatuses,
264
- PaymentStatuses,
265
- FulfillmentStatuses,
266
- AddressTypes,
267
- } from '@behio/storefront-sdk';
268
-
269
- // Product sorting
270
- shop.catalog.getProducts({ sort: ProductSort.PRICE_ASC });
271
- // ProductSort.PRICE_ASC | PRICE_DESC | NAME_ASC | NAME_DESC | NEWEST | FEATURED
272
-
273
- // Order status checks
274
- if (order.status === OrderStatuses.SHIPPED) { ... }
275
- // OrderStatuses.PENDING | CONFIRMED | PROCESSING | SHIPPED | DELIVERED | CANCELLED | REFUNDED
276
-
277
- // Payment status
278
- if (order.paymentStatus === PaymentStatuses.PAID) { ... }
84
+ ---
279
85
 
280
- // Fulfillment
281
- if (order.fulfillmentStatus === FulfillmentStatuses.FULFILLED) { ... }
86
+ # 🎯 Next.js / React path
282
87
 
283
- // Address types
284
- shop.customer.createAddress({ type: AddressTypes.SHIPPING, ... });
88
+ You can build your Next.js storefront in **two ways** — pick one or mix them:
285
89
 
286
- // Iterate for dropdowns/selects
287
- Object.values(ProductSort); // ['price_asc', 'price_desc', ...]
288
- Object.values(OrderStatuses); // ['PENDING', 'CONFIRMED', ...]
289
- ```
90
+ ### [A. Client-side hooks](#1-setup-the-provider) 🪝
91
+ `BehioProvider` + hooks like `useProducts`, `useCart`, `useAuth`. Fast interactivity, automatic state management, optimistic updates.
290
92
 
291
- ### Events
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.
292
95
 
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`));
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.
302
98
 
303
- // Unsubscribe
304
- const unsub = shop.on('error', handler);
305
- unsub(); // stop listening
306
- ```
99
+ **Best for:** SEO-critical pages, checkout, auth forms, sensitive mutations.
100
+ **Upside:** More secure — private API key, better perceived performance, progressive enhancement.
307
101
 
308
- ### Interceptors
102
+ ### Recommendation
309
103
 
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!}
123
+ apiKey={process.env.NEXT_PUBLIC_BEHIO_API_KEY!} // e.g. "pk_live_6d3f255..."
353
124
  baseUrl={process.env.NEXT_PUBLIC_BEHIO_API_URL} // optional — defaults to https://api.behio.com
354
125
  locale="cs"
355
126
  currency="CZK"
@@ -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,67 +149,78 @@ export default function RootLayout({ children }: { children: React.ReactNode })
376
149
  }
377
150
  ```
378
151
 
152
+ Set your env variables:
153
+
379
154
  ```bash
380
155
  # .env.local
381
- NEXT_PUBLIC_BEHIO_API_KEY=pk_live_xxx
382
- NEXT_PUBLIC_BEHIO_API_URL=https://api.behio.com # production
383
- # NEXT_PUBLIC_BEHIO_API_URL=http://localhost:7007 # local dev
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
384
163
  ```
385
164
 
386
- **Optional:** If you prefer to hide the backend URL behind a proxy, add a Next.js rewrite in `next.config.ts` and set `baseUrl="/api"`:
165
+ **Optional:** Hide the backend URL behind a proxy. Add a Next.js rewrite in `next.config.ts` and set `baseUrl="/api"`:
387
166
  ```typescript
388
167
  async rewrites() {
389
168
  return [{ source: '/api/:path*', destination: 'https://api.behio.com/:path*' }];
390
169
  }
391
170
  ```
392
171
 
393
- ### 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 }`.
394
175
 
395
176
  ```tsx
396
177
  'use client';
397
- import { useProducts } from '@behio/storefront-sdk/react';
178
+ import { useShopInfo, useProducts, useProduct, useCategories, formatPrice } from '@behio/storefront-sdk/react';
398
179
 
399
- export function ProductGrid() {
400
- const { data, isLoading } = useProducts({
401
- limit: 24,
402
- category: 'electronics',
403
- sort: 'newest',
404
- });
405
-
406
- 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();
407
184
 
408
185
  return (
409
- <div className="grid grid-cols-4 gap-4">
410
- {data?.items.map(product => (
411
- <div key={product.id}>
412
- <h3>{product.name}</h3>
413
- <p>{product.price.amount} {product.price.currency}</p>
414
- </div>
415
- ))}
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>
416
199
  </div>
417
200
  );
418
201
  }
419
202
  ```
420
203
 
421
- ### useProduct Product detail with SSR prefetch
204
+ **SSR prefetch** (App Router server components):
422
205
 
423
206
  ```tsx
424
- // Server Component — prefetch data
207
+ // app/products/[slug]/page.tsx (server component)
425
208
  import { BehioStorefront } from '@behio/storefront-sdk';
426
209
  import { ProductDetail } from './ProductDetail';
427
210
 
428
- 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;
429
213
  const shop = new BehioStorefront({
430
214
  apiKey: process.env.BEHIO_API_KEY!,
431
215
  baseUrl: process.env.BEHIO_API_URL,
432
216
  });
433
- const initialData = await shop.catalog.getProduct(params.slug);
434
- return <ProductDetail slug={params.slug} initialData={initialData} />;
217
+ const initialData = await shop.catalog.getProduct(slug);
218
+ return <ProductDetail slug={slug} initialData={initialData} />;
435
219
  }
436
220
  ```
437
221
 
438
222
  ```tsx
439
- // Client Component — hydrates with SSR data, then syncs
223
+ // ProductDetail.tsx (client component — hydrates with SSR data)
440
224
  'use client';
441
225
  import { useProduct } from '@behio/storefront-sdk/react';
442
226
 
@@ -446,29 +230,35 @@ export function ProductDetail({ slug, initialData }) {
446
230
  }
447
231
  ```
448
232
 
449
- ### 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.
450
238
 
451
239
  ```tsx
452
240
  'use client';
453
- import { useCart, formatPrice } from '@behio/storefront-sdk/react';
241
+ import { useCart, useCartCount, formatPrice } from '@behio/storefront-sdk/react';
242
+
243
+ export function CartButton() {
244
+ const count = useCartCount(); // lightweight — just the number
245
+ return <span>Cart ({count})</span>;
246
+ }
454
247
 
455
- export function Cart() {
248
+ export function CartPage() {
456
249
  const {
457
250
  cart, isEmpty, itemCount,
458
251
  addItem, updateQuantity, removeItem, clear,
459
252
  applyDiscount, removeDiscount,
460
- isAdding, isUpdating,
461
253
  } = useCart();
462
254
 
463
255
  if (isEmpty) return <p>Your cart is empty</p>;
464
256
 
465
257
  return (
466
258
  <div>
467
- <h2>Cart ({itemCount})</h2>
468
259
  {cart?.items.map(item => (
469
260
  <div key={item.id}>
470
261
  <span>{item.product.name}</span>
471
- <span>{formatPrice(item.unitPrice, cart.currency)}</span>
472
262
  <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
473
263
  <span>{item.quantity}</span>
474
264
  <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
@@ -481,19 +271,38 @@ export function Cart() {
481
271
  }
482
272
  ```
483
273
 
484
- ### useCartCount — Lightweight badge
274
+ Checkout:
485
275
 
486
276
  ```tsx
487
277
  'use client';
488
- import { useCartCount } from '@behio/storefront-sdk/react';
278
+ import { useCheckout } from '@behio/storefront-sdk/react';
489
279
 
490
- export function NavCartBadge() {
491
- const count = useCartCount();
492
- 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
+ );
493
298
  }
494
299
  ```
495
300
 
496
- ### 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.
497
306
 
498
307
  ```tsx
499
308
  'use client';
@@ -504,10 +313,10 @@ export function AuthSection() {
504
313
 
505
314
  if (isLoggedIn) {
506
315
  return (
507
- <div>
316
+ <>
508
317
  <p>Welcome, {customer?.firstName || customer?.email}</p>
509
318
  <button onClick={logout}>Logout</button>
510
- </div>
319
+ </>
511
320
  );
512
321
  }
513
322
 
@@ -517,63 +326,63 @@ export function AuthSection() {
517
326
  const fd = new FormData(e.currentTarget);
518
327
  await login(fd.get('email') as string, fd.get('password') as string);
519
328
  }}>
520
- <input name="email" type="email" placeholder="Email" />
521
- <input name="password" type="password" placeholder="Password" />
329
+ <input name="email" type="email" />
330
+ <input name="password" type="password" />
522
331
  {loginError && <p>{loginError.message}</p>}
523
- <button disabled={isLoggingIn}>{isLoggingIn ? 'Logging in...' : 'Login'}</button>
332
+ <button disabled={isLoggingIn}>Login</button>
524
333
  </form>
525
334
  );
526
335
  }
527
336
  ```
528
337
 
529
- ### useCheckout Order creation
338
+ Once logged in, authenticated hooks work automatically:
530
339
 
531
340
  ```tsx
532
- 'use client';
533
- import { useCheckout } from '@behio/storefront-sdk/react';
341
+ import { useOrders, useCustomer, useAddresses } from '@behio/storefront-sdk/react';
534
342
 
535
- export function CheckoutForm() {
536
- const { createOrder, isCreating, order, reset } = useCheckout();
343
+ const { data: orders } = useOrders();
344
+ const { data: profile, updateProfile } = useCustomer();
345
+ const { addresses, createAddress, deleteAddress } = useAddresses();
346
+ ```
537
347
 
538
- if (order) return <h2>Order #{order.orderNumber} confirmed!</h2>;
348
+ [Auto token refresh](#auto-token-refresh)
539
349
 
540
- return (
541
- <form onSubmit={async (e) => {
542
- e.preventDefault();
543
- await createOrder({ email: '...', shippingAddress: { ... }, billingAddress: { ... } });
544
- }}>
545
- {/* form fields */}
546
- <button disabled={isCreating}>{isCreating ? 'Processing...' : 'Place Order'}</button>
547
- </form>
548
- );
549
- }
550
- ```
350
+ <a id="b-server-components--server-actions"></a>
351
+ ## 5. Server Components & Server Actions (secure path)
352
+
353
+ For the **most secure** integration — API key stays on the server, tokens in httpOnly cookies, forms work without JavaScript (progressive enhancement).
551
354
 
552
- ### useSearchDebounced search
355
+ **Server Componentdata fetching:**
553
356
 
554
357
  ```tsx
555
- 'use client';
556
- import { useSearch } from '@behio/storefront-sdk/react';
557
- 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
+ });
558
366
 
559
- function SearchBar() {
560
- const [query, setQuery] = useState('');
561
- const { data, isLoading } = useSearch(query, { debounceMs: 400, limit: 5 });
367
+ const products = await shop.catalog.getProducts({ limit: 24 });
562
368
 
563
369
  return (
564
370
  <div>
565
- <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
566
- {isLoading && <span>Searching...</span>}
567
- {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
+ ))}
568
377
  </div>
569
378
  );
570
379
  }
571
380
  ```
572
381
 
573
- ### Server Actions + Forms
382
+ **Server Actions mutations:**
574
383
 
575
384
  ```tsx
576
- // app/actions.ts — server actions for checkout, auth, etc.
385
+ // app/actions.ts
577
386
  'use server';
578
387
  import { BehioStorefront } from '@behio/storefront-sdk';
579
388
  import { cookies } from 'next/headers';
@@ -585,23 +394,6 @@ function getShop() {
585
394
  });
586
395
  }
587
396
 
588
- export async function registerAction(formData: FormData) {
589
- const shop = getShop();
590
- const result = await shop.auth.register({
591
- email: formData.get('email') as string,
592
- password: formData.get('password') as string,
593
- firstName: formData.get('firstName') as string,
594
- lastName: formData.get('lastName') as string,
595
- });
596
-
597
- // Store tokens in httpOnly cookies (secure, server-side)
598
- const cookieStore = await cookies();
599
- cookieStore.set('behio_access', result.accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900 });
600
- cookieStore.set('behio_refresh', result.refreshToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 86400 });
601
-
602
- return { success: true };
603
- }
604
-
605
397
  export async function loginAction(_: unknown, formData: FormData) {
606
398
  const shop = getShop();
607
399
  try {
@@ -611,8 +403,12 @@ export async function loginAction(_: unknown, formData: FormData) {
611
403
  });
612
404
 
613
405
  const cookieStore = await cookies();
614
- cookieStore.set('behio_access', result.accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900 });
615
- 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
+ });
616
412
 
617
413
  return { success: true, error: null };
618
414
  } catch (err) {
@@ -620,64 +416,25 @@ export async function loginAction(_: unknown, formData: FormData) {
620
416
  }
621
417
  }
622
418
 
623
- export async function checkoutAction(formData: FormData) {
419
+ export async function addToCartAction(productId: string, quantity = 1) {
624
420
  const shop = getShop();
625
-
626
- // Restore auth from cookies
627
- const cookieStore = await cookies();
628
- const accessToken = cookieStore.get('behio_access')?.value;
629
- const cartSession = cookieStore.get('behio_cart_session')?.value;
630
- if (accessToken) shop.setTokens({ accessToken, refreshToken: '' });
631
- if (cartSession) shop.setCartSession(cartSession);
632
-
633
- const order = await shop.checkout.createOrder({
634
- email: formData.get('email') as string,
635
- phone: formData.get('phone') as string || undefined,
636
- customerNote: formData.get('note') as string || undefined,
637
- shippingAddress: {
638
- firstName: formData.get('shipping_firstName') as string,
639
- lastName: formData.get('shipping_lastName') as string,
640
- street: formData.get('shipping_street') as string,
641
- city: formData.get('shipping_city') as string,
642
- zip: formData.get('shipping_zip') as string,
643
- country: formData.get('shipping_country') as string,
644
- },
645
- billingAddress: {
646
- firstName: formData.get('billing_firstName') as string,
647
- lastName: formData.get('billing_lastName') as string,
648
- street: formData.get('billing_street') as string,
649
- city: formData.get('billing_city') as string,
650
- zip: formData.get('billing_zip') as string,
651
- country: formData.get('billing_country') as string,
652
- },
653
- });
654
-
655
- // Clear cart cookie
656
- cookieStore.delete('behio_cart_session');
657
-
658
- return { orderNumber: order.orderNumber, trackingToken: order.trackingToken };
659
- }
660
-
661
- export async function addToCartAction(productId: string, quantity: number = 1) {
662
- const shop = getShop();
663
-
664
421
  const cookieStore = await cookies();
665
422
  const cartSession = cookieStore.get('behio_cart_session')?.value;
666
423
  if (cartSession) shop.setCartSession(cartSession);
667
424
 
668
425
  const result = await shop.cart.addItem({ productId, quantity });
669
426
 
670
- // Save session token to cookie
671
427
  if (result.newSessionToken) {
672
- 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
+ });
673
431
  }
674
-
675
432
  return result;
676
433
  }
677
434
  ```
678
435
 
679
436
  ```tsx
680
- // app/auth/page.tsx — Login form with useActionState
437
+ // app/auth/page.tsx
681
438
  'use client';
682
439
  import { useActionState } from 'react';
683
440
  import { loginAction } from '../actions';
@@ -685,297 +442,485 @@ import { loginAction } from '../actions';
685
442
  export default function LoginPage() {
686
443
  const [state, formAction, isPending] = useActionState(loginAction, { success: false, error: null });
687
444
 
688
- if (state.success) return <p>Logged in! Redirecting...</p>;
689
-
690
445
  return (
691
446
  <form action={formAction}>
692
447
  <input name="email" type="email" placeholder="Email" required />
693
448
  <input name="password" type="password" placeholder="Password" required />
694
- {state.error && <p className="text-red-500">{state.error}</p>}
449
+ {state.error && <p>{state.error}</p>}
695
450
  <button disabled={isPending}>{isPending ? 'Logging in...' : 'Login'}</button>
696
451
  </form>
697
452
  );
698
453
  }
699
454
  ```
700
455
 
701
- ```tsx
702
- // app/register/page.tsx — Register with progressive enhancement
703
- import { registerAction } from '../actions';
704
- import { redirect } from 'next/navigation';
705
-
706
- export default function RegisterPage() {
707
- async function handleRegister(formData: FormData) {
708
- 'use server';
709
- await registerAction(formData);
710
- redirect('/account');
711
- }
456
+ ---
712
457
 
713
- return (
714
- <form action={handleRegister}>
715
- <input name="firstName" placeholder="First name" />
716
- <input name="lastName" placeholder="Last name" />
717
- <input name="email" type="email" placeholder="Email" required />
718
- <input name="password" type="password" placeholder="Password" required />
719
- <button type="submit">Create Account</button>
720
- </form>
721
- );
722
- }
723
- ```
458
+ # 🛠 Vanilla JS / Node.js path
724
459
 
725
- ```tsx
726
- // app/products/[slug]/page.tsx — Add to cart with server action
460
+ For Node.js backends, serverless functions, CLI tools, Deno, Bun, or any non-React JavaScript environment.
461
+
462
+ ## 1. Create a client
463
+
464
+ ```typescript
727
465
  import { BehioStorefront } from '@behio/storefront-sdk';
728
- import { addToCartAction } from '../../actions';
729
- import { formatPrice } from '@behio/storefront-sdk/react';
730
466
 
731
- export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
732
- const { slug } = await params;
733
- const shop = new BehioStorefront({ apiKey: process.env.BEHIO_API_KEY!, baseUrl: process.env.BEHIO_API_URL });
734
- const product = await shop.catalog.getProduct(slug);
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
+ });
475
+ ```
735
476
 
736
- return (
737
- <div>
738
- <h1>{product.name}</h1>
739
- <p>{product.longDescription}</p>
740
- <p className="text-2xl font-bold">{formatPrice(product.price.amount, product.price.currency)}</p>
741
-
742
- {product.price.compareAtPrice && (
743
- <p className="line-through text-gray-500">
744
- {formatPrice(product.price.compareAtPrice, product.price.currency)}
745
- </p>
746
- )}
747
-
748
- <form action={async () => {
749
- 'use server';
750
- await addToCartAction(product.id);
751
- }}>
752
- <button type="submit" disabled={!product.inStock}>
753
- {product.inStock ? 'Add to Cart' : 'Out of Stock'}
754
- </button>
755
- </form>
756
-
757
- {product.volumePricing.length > 0 && (
758
- <table>
759
- <thead><tr><th>Quantity</th><th>Price</th></tr></thead>
760
- <tbody>
761
- {product.volumePricing.map(vp => (
762
- <tr key={vp.minQuantity}>
763
- <td>{vp.minQuantity}+</td>
764
- <td>{formatPrice(vp.price, product.price.currency)}</td>
765
- </tr>
766
- ))}
767
- </tbody>
768
- </table>
769
- )}
770
- </div>
771
- );
772
- }
477
+ ## 2. Call any endpoint
478
+
479
+ Every API method returns a Promise. Types are fully auto-completed.
480
+
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();
486
+
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');
773
509
  ```
774
510
 
775
- ```tsx
776
- // app/checkout/page.tsx — Full checkout form with server action
777
- import { checkoutAction } from '../actions';
778
- import { redirect } from 'next/navigation';
779
-
780
- export default function CheckoutPage() {
781
- async function handleCheckout(formData: FormData) {
782
- 'use server';
783
- const result = await checkoutAction(formData);
784
- redirect(`/orders/confirmation?order=${result.orderNumber}`);
785
- }
511
+ → [All API methods](#all-api-methods)
786
512
 
787
- return (
788
- <form action={handleCheckout}>
789
- <h2>Contact</h2>
790
- <input name="email" type="email" placeholder="Email" required />
791
- <input name="phone" type="tel" placeholder="Phone" />
792
-
793
- <h2>Shipping Address</h2>
794
- <input name="shipping_firstName" placeholder="First name" required />
795
- <input name="shipping_lastName" placeholder="Last name" required />
796
- <input name="shipping_street" placeholder="Street" required />
797
- <div style={{ display: 'flex', gap: 8 }}>
798
- <input name="shipping_city" placeholder="City" required />
799
- <input name="shipping_zip" placeholder="ZIP" required />
800
- </div>
801
- <input name="shipping_country" placeholder="Country code (CZ)" defaultValue="CZ" required />
802
-
803
- <h2>Billing Address</h2>
804
- <input name="billing_firstName" placeholder="First name" required />
805
- <input name="billing_lastName" placeholder="Last name" required />
806
- <input name="billing_street" placeholder="Street" required />
807
- <div style={{ display: 'flex', gap: 8 }}>
808
- <input name="billing_city" placeholder="City" required />
809
- <input name="billing_zip" placeholder="ZIP" required />
810
- </div>
811
- <input name="billing_country" placeholder="Country code (CZ)" defaultValue="CZ" required />
513
+ ---
812
514
 
813
- <textarea name="note" placeholder="Order note (optional)" />
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
+ ```
814
546
 
815
- <button type="submit">Place Order</button>
816
- </form>
817
- );
818
- }
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>
819
562
  ```
820
563
 
821
- ### All hooks
822
-
823
- | Hook | Purpose | Auth |
824
- |------|---------|------|
825
- | `useShopInfo()` | E-shop info | No |
826
- | `useProducts(query?)` | Product list with filters | No |
827
- | `useProduct(slug)` | Product detail | No |
828
- | `useCategories(locale?)` | Category tree | No |
829
- | `useLabels(locale?)` | Labels/tags | No |
830
- | `useFeatured()` | Featured products | No |
831
- | `useFilters()` | Dynamic filter fields | No |
832
- | `useSearch(query)` | Debounced search | No |
833
- | `useCart()` | Cart state + actions | No |
834
- | `useCartCount()` | Item count (lightweight) | No |
835
- | `useAuth()` | Login, register, logout | No |
836
- | `useCheckout()` | Create order | No* |
837
- | `useOrders()` | Order list | Yes |
838
- | `useOrder(num)` | Order detail + cancel | Yes |
839
- | `useCustomer()` | Profile + update | Yes |
840
- | `useAddresses()` | Address CRUD | Yes |
841
- | `usePages()` / `usePage(slug)` | CMS pages | No |
842
-
843
- \* Guest checkout works without auth if e-shop allows it.
844
-
845
- ### 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
+ ```
846
573
 
574
+ ### useCategories
847
575
  ```typescript
848
- import { formatPrice } from '@behio/storefront-sdk/react';
576
+ const { data, isLoading } = useCategories('cs');
577
+ // data: Category[] (tree structure with children)
578
+ ```
849
579
 
850
- formatPrice(1499, 'CZK'); // "1 499,00 Kč"
851
- formatPrice(24.99, 'EUR'); // "24,99 €"
852
- formatPrice(1499, 'CZK', 'en'); // "CZK 1,499.00"
580
+ ### useLabels
581
+ ```typescript
582
+ const { data } = useLabels('cs');
583
+ // data: ProductLabel[]
853
584
  ```
854
585
 
855
- ---
586
+ ### useFeatured
587
+ ```typescript
588
+ const { data } = useFeatured();
589
+ // data: PaginatedResponse<ProductListItem>
590
+ ```
856
591
 
857
- ## How it works
592
+ ### useFilters
593
+ ```typescript
594
+ const { data } = useFilters();
595
+ // data: FilterField[] — use to build dynamic filter UI
596
+ ```
858
597
 
859
- - **Auth**: API key in `X-Api-Key` header identifies your e-shop. Customer JWT in `Authorization` header identifies the logged-in customer.
860
- - **Cart sessions**: Anonymous carts use `X-Cart-Session` header. Session token auto-managed by SDK.
861
- - **Token refresh**: 401 automatic refresh retry. Transparent to your code.
862
- - **Cart merge**: After login, anonymous cart automatically merged into customer cart.
863
- - **React Query**: All hooks use TanStack Query — caching, deduplication, background refetch.
864
- - **Optimistic updates**: Cart remove/update reflected instantly, rollback on error.
865
- - **SSR**: Pass `initialData` to hooks for server-prefetched data.
866
- - **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
+ ```
867
603
 
868
- ---
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
+ ```
629
+
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
+ ```
654
+
655
+ ### useOrders
656
+ ```typescript
657
+ const { data, isLoading } = useOrders({ page: 1, limit: 10 });
658
+ // data: PaginatedResponse<OrderListItem>
659
+ ```
660
+
661
+ ### useOrder
662
+ ```typescript
663
+ const { data, cancel, isCancelling } = useOrder('ORD-123');
664
+ // data: OrderDetail
665
+ ```
666
+
667
+ ### useCustomer
668
+ ```typescript
669
+ const { data, updateProfile, isUpdating } = useCustomer();
670
+ // data: CustomerProfile
671
+ ```
869
672
 
870
- ## API Reference (for AI agents)
673
+ ### useAddresses
674
+ ```typescript
675
+ const {
676
+ addresses,
677
+ createAddress, updateAddress, deleteAddress,
678
+ isCreating, isDeleting,
679
+ } = useAddresses();
680
+ ```
871
681
 
872
- ### Package exports
682
+ ### usePages
683
+ ```typescript
684
+ const { data: pages } = usePages('cs');
685
+ const { data: page } = usePage('about-us', 'cs');
873
686
  ```
874
- @behio/storefront-sdk → BehioStorefront, types, BehioApiError, BehioNetworkError
875
- @behio/storefront-sdk/react → BehioProvider, all hooks, formatPrice
687
+
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>
876
701
  ```
877
702
 
878
- ### Constructor
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
+ ```
714
+
715
+ ### Cart
879
716
  ```typescript
880
- new BehioStorefront({ apiKey, baseUrl?, locale?, currency?, timeout?, retries?, retryDelay?, fetch? })
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
881
725
  ```
882
726
 
883
- ### Instance methods
727
+ ### Checkout
728
+ ```typescript
729
+ shop.checkout.createOrder(input) → OrderDetail
884
730
  ```
885
- shop.getShopInfo() → ShopInfo
886
- shop.setTokens({ accessToken, refreshToken }) / shop.clearTokens()
887
- shop.setCartSession(token) / shop.getCartSession() / shop.clearCartSession()
888
- shop.on(event, handler) unsubscribe fn
889
- shop.addRequestInterceptor(fn) unsubscribe fn
890
- shop.addResponseInterceptor(fn) unsubscribe fn
891
- shop.getRateLimitInfo() { remaining, reset }
731
+
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
892
738
  ```
893
739
 
894
- ### Sub-modules
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
895
749
  ```
896
- shop.catalog.getProducts(query?) → PaginatedResponse<ProductListItem>
897
- shop.catalog.getProduct(slug) → ProductDetail
898
- shop.catalog.getCategories(locale?) → { categories: Category[] }
899
- shop.catalog.getCategory(slug) CategoryDetail
900
- shop.catalog.getCategoryProducts(slug, query?) PaginatedResponse<ProductListItem>
901
- shop.catalog.getLabels(locale?) → { labels: ProductLabel[] }
902
- shop.catalog.getFeatured() → PaginatedResponse<ProductListItem>
903
- shop.catalog.getFilters() → { filters: FilterField[] }
904
- shop.catalog.search(query, opts?) → PaginatedResponse<ProductListItem>
905
-
906
- shop.auth.register(input) → AuthTokens
907
- shop.auth.login(input) → AuthTokens
908
- shop.auth.refresh(token?) → AuthTokens
909
- shop.auth.logout(token?) → MessageResponse
910
- shop.auth.forgotPassword(email) → MessageResponse
911
- shop.auth.resetPassword(token, password) → MessageResponse
912
- shop.auth.verifyEmail(token) → MessageResponse
913
- shop.auth.isLoggedIn() → boolean
914
-
915
- shop.cart.get() → Cart
916
- shop.cart.addItem({ productId, quantity }) → Cart & { newSessionToken? }
917
- shop.cart.updateQuantity(itemId, qty) → Cart
918
- shop.cart.removeItem(itemId) → Cart
919
- shop.cart.clear() → void
920
- shop.cart.merge() → Cart
921
- shop.cart.applyDiscount(code) → Cart
922
- shop.cart.removeDiscount() → Cart
923
-
924
- shop.checkout.createOrder(input) → OrderDetail
925
-
926
- shop.orders.list(opts?) → PaginatedResponse<OrderListItem>
927
- shop.orders.get(orderNumber) → OrderDetail
928
- shop.orders.cancel(orderNumber) → OrderDetail
929
- shop.orders.track(trackingToken) → OrderDetail
930
-
931
- shop.customer.getProfile() → CustomerProfile
932
- shop.customer.updateProfile(data) → CustomerProfile
933
- shop.customer.changePassword(current, new) → MessageResponse
934
- shop.customer.getAddresses() → { items: CustomerAddress[] }
935
- shop.customer.createAddress(data) → CustomerAddress
936
- shop.customer.updateAddress(id, data) → CustomerAddress
937
- shop.customer.deleteAddress(id) → void
938
-
939
- shop.pages.list(locale?) → { pages: Page[] }
940
- shop.pages.get(slug, locale?) → PageDetail
750
+
751
+ ### Pages
752
+ ```typescript
753
+ shop.pages.list(locale?) { pages: Page[] }
754
+ shop.pages.get(slug, locale?) PageDetail
941
755
  ```
942
756
 
943
- ### Key types
757
+ ### Instance utilities
944
758
  ```typescript
945
- ProductListItem { id, slug, name, shortDescription?, sku, gtin?, price: ProductPrice, inStock, stockQuantity?, image?, labels[], isFeatured }
946
- ProductDetail extends ProductListItem { longDescription?, images[], categories[], variants[], volumePricing[], customFields, seo, weight?, weightUnit? }
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
+
775
+ ```typescript
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
+
947
790
  ProductPrice { amount: number, currency: string, compareAtPrice?: number | null }
948
- Cart { id, sessionToken?, items: CartItem[], subtotal, discountTotal, discount?, grandTotal, currency, itemCount }
949
- CartItem { id, product: CartItemProduct, quantity, unitPrice, totalPrice, priceChanged, volumePriceApplied }
950
- OrderDetail { orderNumber, status, paymentStatus, fulfillmentStatus, items[], addresses, totals, statusHistory[], trackingToken? }
951
- CustomerProfile { id, email, firstName?, lastName?, phone?, emailVerified }
952
- BehioApiError { status, code: BehioErrorCode, body, message, isRetryable, is(code) }
953
- 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
+ }
954
815
  ```
955
816
 
956
- ### Const enums (runtime + type-safe)
817
+ ## Constants / Enums
818
+
819
+ Type-safe constants for common values — autocomplete, iterable, zero runtime cost.
820
+
957
821
  ```typescript
958
- ProductSort { PRICE_ASC, PRICE_DESC, NAME_ASC, NAME_DESC, NEWEST, FEATURED }
959
- OrderStatuses { PENDING, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED, REFUNDED }
960
- PaymentStatuses { UNPAID, PAID, PARTIALLY_REFUNDED, REFUNDED }
961
- FulfillmentStatuses { UNFULFILLED, PARTIALLY_FULFILLED, FULFILLED }
962
- 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', ...]
963
840
  ```
964
841
 
965
- ### 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
+
966
851
  ```typescript
967
- BehioProviderProps {
968
- apiKey: string,
969
- baseUrl?: string,
970
- locale?: string,
971
- currency?: string,
972
- storage?: 'cookies' | 'localStorage' | 'memory' | StorageAdapter,
973
- 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
+ }
974
869
  }
975
870
  ```
976
871
 
977
- ### 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
+ });
978
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
979
924
  ['behio', 'shop-info']
980
925
  ['behio', 'products', queryString]
981
926
  ['behio', 'product', slug]
@@ -993,34 +938,6 @@ BehioProviderProps {
993
938
  ['behio', 'page', slug, locale?]
994
939
  ```
995
940
 
996
- ### Event types
997
- ```
998
- 'auth:login' | 'auth:logout' | 'auth:token-refresh' | 'auth:token-refresh-failed'
999
- 'cart:updated' | 'cart:cleared' | 'order:created'
1000
- 'error' | 'request' | 'response' | 'rate-limit-warning'
1001
- ```
1002
-
1003
- ---
1004
-
1005
- ## Next.js full example
1006
-
1007
- ```
1008
- app/
1009
- ├── layout.tsx ← BehioProvider
1010
- ├── page.tsx ← useShopInfo + useFeatured
1011
- ├── products/
1012
- │ ├── page.tsx ← useProducts
1013
- │ └── [slug]/page.tsx ← useProduct (SSR prefetch)
1014
- ├── cart/page.tsx ← useCart
1015
- ├── checkout/page.tsx ← useCheckout
1016
- ├── auth/page.tsx ← useAuth
1017
- ├── account/
1018
- │ ├── page.tsx ← useCustomer
1019
- │ ├── orders/page.tsx ← useOrders
1020
- │ └── addresses/page.tsx ← useAddresses
1021
- └── [slug]/page.tsx ← usePage (CMS)
1022
- ```
1023
-
1024
941
  ## License
1025
942
 
1026
943
  MIT