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