@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 +999 -0
- package/dist/chunk-26WZOAWP.js +609 -0
- package/dist/chunk-3OJEEMLI.mjs +609 -0
- package/dist/index.d.mts +566 -0
- package/dist/index.d.ts +566 -0
- package/dist/index.js +20 -0
- package/dist/index.mjs +20 -0
- package/dist/react.d.mts +166 -0
- package/dist/react.d.ts +166 -0
- package/dist/react.js +813 -0
- package/dist/react.mjs +813 -0
- package/package.json +61 -0
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
|