@behio/storefront-sdk 0.1.0

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