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