@behio/storefront-sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -1340
- package/dist/{chunk-EFFPOXWL.mjs → chunk-45FTJKSW.mjs} +19 -0
- package/dist/{chunk-R7H7E5BX.js → chunk-ZEPWNT56.js} +19 -0
- package/dist/index.d.mts +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +51 -2
- package/dist/react.d.ts +51 -2
- package/dist/react.js +92 -27
- package/dist/react.mjs +111 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,1393 +1,120 @@
|
|
|
1
1
|
# @behio/storefront-sdk
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Headless e-commerce SDK for building custom storefronts.**
|
|
4
4
|
|
|
5
|
-
Behio
|
|
6
|
-
|
|
7
|
-
This SDK is the fastest way to connect your Next.js, React, or any JavaScript app to the Behio Storefront API.
|
|
5
|
+
Behio gives you a complete e-commerce backend — products, inventory, orders, customers, discounts, multi-currency, multi-language — and lets you design the storefront however you want. No themes, no templates, no vendor lock-in.
|
|
8
6
|
|
|
9
7
|
## Why Behio?
|
|
10
8
|
|
|
11
|
-
- **You own the frontend.**
|
|
12
|
-
- **Production-ready
|
|
13
|
-
- **Built for developers.**
|
|
14
|
-
- **Scale-ready.** Redis caching, rate limiting, webhooks,
|
|
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
|
-
|
|
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.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## 🚀 Get started
|
|
22
|
-
|
|
23
|
-
Pick your integration path:
|
|
24
|
-
|
|
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.
|
|
27
|
-
|
|
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.
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## 📚 Table of contents
|
|
34
|
-
|
|
35
|
-
**Setup**
|
|
36
|
-
- [Install](#install)
|
|
37
|
-
- [Get your API key](#get-your-api-key)
|
|
38
|
-
|
|
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)
|
|
42
|
-
|
|
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)
|
|
52
|
-
|
|
53
|
-
---
|
|
9
|
+
- **You own the frontend.** Next.js, React, Vue, Nuxt, Astro, or plain JS — the backend doesn't care.
|
|
10
|
+
- **Production-ready in minutes.** Catalog, cart, checkout, customer accounts, orders, CMS, discount codes, gift cards, loyalty programs, and more.
|
|
11
|
+
- **Built for developers.** Full TypeScript types, auto-completing, modern React hooks with TanStack Query.
|
|
12
|
+
- **Scale-ready.** Redis caching, rate limiting, webhooks, atomic checkout (no double-spend, no overselling).
|
|
54
13
|
|
|
55
14
|
## Install
|
|
56
15
|
|
|
57
16
|
```bash
|
|
58
17
|
npm install @behio/storefront-sdk
|
|
59
|
-
# or
|
|
60
|
-
yarn add @behio/storefront-sdk
|
|
61
|
-
# or
|
|
62
|
-
pnpm add @behio/storefront-sdk
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The SDK has two entry points:
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
// Core — works everywhere (Node.js, browser, Deno, Bun, edge runtimes)
|
|
69
|
-
import { BehioStorefront } from '@behio/storefront-sdk';
|
|
70
|
-
|
|
71
|
-
// React — hooks + provider for Next.js / React apps
|
|
72
|
-
import { BehioProvider, useProducts, useCart, useAuth } from '@behio/storefront-sdk/react';
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Get your API key
|
|
76
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
The API key identifies your e-shop. No shop domain needed.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
# 🎯 Next.js / React path
|
|
87
|
-
|
|
88
|
-
You can build your Next.js storefront in **two ways** — pick one or mix them:
|
|
89
|
-
|
|
90
|
-
### [A. Client-side hooks](#1-setup-the-provider) 🪝
|
|
91
|
-
`BehioProvider` + hooks like `useProducts`, `useCart`, `useAuth`. Fast interactivity, automatic state management, optimistic updates.
|
|
92
|
-
|
|
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.
|
|
95
|
-
|
|
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.
|
|
98
|
-
|
|
99
|
-
**Best for:** SEO-critical pages, checkout, auth forms, sensitive mutations.
|
|
100
|
-
**Upside:** More secure — private API key, better perceived performance, progressive enhancement.
|
|
101
|
-
|
|
102
|
-
### Recommendation
|
|
103
|
-
|
|
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)
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## 1. Setup the provider
|
|
112
|
-
|
|
113
|
-
Create a client wrapper for the provider:
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
// app/providers.tsx
|
|
117
|
-
'use client';
|
|
118
|
-
import { BehioProvider } from '@behio/storefront-sdk/react';
|
|
119
|
-
|
|
120
|
-
export function Providers({ children }: { children: React.ReactNode }) {
|
|
121
|
-
return (
|
|
122
|
-
<BehioProvider
|
|
123
|
-
apiKey={process.env.NEXT_PUBLIC_BEHIO_API_KEY!} // e.g. "pk_live_6d3f255..."
|
|
124
|
-
locale="cs"
|
|
125
|
-
currency="CZK"
|
|
126
|
-
storage="cookies" // "cookies" | "localStorage" | "memory"
|
|
127
|
-
>
|
|
128
|
-
{children}
|
|
129
|
-
</BehioProvider>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Use it in your root layout (stays as a server component):
|
|
135
|
-
|
|
136
|
-
```tsx
|
|
137
|
-
// app/layout.tsx
|
|
138
|
-
import { Providers } from './providers';
|
|
139
|
-
|
|
140
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
141
|
-
return (
|
|
142
|
-
<html>
|
|
143
|
-
<body>
|
|
144
|
-
<Providers>{children}</Providers>
|
|
145
|
-
</body>
|
|
146
|
-
</html>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Set your env variables:
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
# .env.local
|
|
155
|
-
NEXT_PUBLIC_BEHIO_API_KEY=pk_live_xxxxxxxxxxxx
|
|
156
|
-
|
|
157
|
-
# For server-side (Server Actions, RSC) — NEVER expose to client
|
|
158
|
-
BEHIO_API_KEY=sk_live_xxxxxxxxxxxx
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## 2. Fetch data with hooks
|
|
162
|
-
|
|
163
|
-
All catalog data available via React hooks. Each hook returns `{ data, isLoading, error, refetch }`.
|
|
164
|
-
|
|
165
|
-
```tsx
|
|
166
|
-
'use client';
|
|
167
|
-
import { useShopInfo, useProducts, useProduct, useCategories, formatPrice } from '@behio/storefront-sdk/react';
|
|
168
|
-
|
|
169
|
-
export function HomePage() {
|
|
170
|
-
const { data: shop } = useShopInfo();
|
|
171
|
-
const { data: products } = useProducts({ limit: 12, sort: 'newest' });
|
|
172
|
-
const { data: cats } = useCategories();
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<div>
|
|
176
|
-
<h1>{shop?.name}</h1>
|
|
177
|
-
<nav>
|
|
178
|
-
{cats?.map(c => <a key={c.id} href={`/category/${c.slug}`}>{c.name}</a>)}
|
|
179
|
-
</nav>
|
|
180
|
-
<div className="grid grid-cols-4 gap-4">
|
|
181
|
-
{products?.items.map(p => (
|
|
182
|
-
<div key={p.id}>
|
|
183
|
-
<h3>{p.name}</h3>
|
|
184
|
-
<p>{formatPrice(p.price.amount, p.price.currency)}</p>
|
|
185
|
-
</div>
|
|
186
|
-
))}
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
18
|
```
|
|
192
19
|
|
|
193
|
-
|
|
20
|
+
## Quick Start
|
|
194
21
|
|
|
195
22
|
```tsx
|
|
196
|
-
// app/products/[slug]/page.tsx (server component)
|
|
197
23
|
import { BehioStorefront } from '@behio/storefront-sdk';
|
|
198
|
-
import { ProductDetail } from './ProductDetail';
|
|
199
24
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const shop = new BehioStorefront({
|
|
203
|
-
apiKey: process.env.BEHIO_API_KEY!,
|
|
204
|
-
});
|
|
205
|
-
const initialData = await shop.catalog.getProduct(slug);
|
|
206
|
-
return <ProductDetail slug={slug} initialData={initialData} />;
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
```tsx
|
|
211
|
-
// ProductDetail.tsx (client component — hydrates with SSR data)
|
|
212
|
-
'use client';
|
|
213
|
-
import { useProduct } from '@behio/storefront-sdk/react';
|
|
214
|
-
|
|
215
|
-
export function ProductDetail({ slug, initialData }) {
|
|
216
|
-
const { data: product } = useProduct(slug, { initialData });
|
|
217
|
-
return <h1>{product?.name}</h1>;
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
→ [All available hooks](#all-react-hooks)
|
|
222
|
-
|
|
223
|
-
## 3. Cart & checkout
|
|
224
|
-
|
|
225
|
-
`useCart` gives you everything — state, mutations, computed values, optimistic updates.
|
|
226
|
-
|
|
227
|
-
```tsx
|
|
228
|
-
'use client';
|
|
229
|
-
import { useCart, useCartCount, formatPrice } from '@behio/storefront-sdk/react';
|
|
230
|
-
|
|
231
|
-
export function CartButton() {
|
|
232
|
-
const count = useCartCount(); // lightweight — just the number
|
|
233
|
-
return <span>Cart ({count})</span>;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function CartPage() {
|
|
237
|
-
const {
|
|
238
|
-
cart, isEmpty, itemCount,
|
|
239
|
-
addItem, updateQuantity, removeItem, clear,
|
|
240
|
-
applyDiscount, removeDiscount,
|
|
241
|
-
} = useCart();
|
|
242
|
-
|
|
243
|
-
if (isEmpty) return <p>Your cart is empty</p>;
|
|
244
|
-
|
|
245
|
-
return (
|
|
246
|
-
<div>
|
|
247
|
-
{cart?.items.map(item => (
|
|
248
|
-
<div key={item.id}>
|
|
249
|
-
<span>{item.product.name}</span>
|
|
250
|
-
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
|
|
251
|
-
<span>{item.quantity}</span>
|
|
252
|
-
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
|
|
253
|
-
<button onClick={() => removeItem(item.id)}>Remove</button>
|
|
254
|
-
</div>
|
|
255
|
-
))}
|
|
256
|
-
<p>Total: {formatPrice(cart?.grandTotal ?? 0, cart?.currency ?? 'CZK')}</p>
|
|
257
|
-
</div>
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Checkout:
|
|
263
|
-
|
|
264
|
-
```tsx
|
|
265
|
-
'use client';
|
|
266
|
-
import { useCheckout } from '@behio/storefront-sdk/react';
|
|
267
|
-
|
|
268
|
-
export function CheckoutForm() {
|
|
269
|
-
const { createOrder, isCreating, order } = useCheckout();
|
|
270
|
-
|
|
271
|
-
if (order) return <h2>Order #{order.orderNumber} confirmed!</h2>;
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<form onSubmit={async (e) => {
|
|
275
|
-
e.preventDefault();
|
|
276
|
-
await createOrder({
|
|
277
|
-
email: 'customer@example.com',
|
|
278
|
-
shippingAddress: { firstName: 'Jan', lastName: 'Novak', street: 'Hlavni 1', city: 'Praha', zip: '11000', country: 'CZ' },
|
|
279
|
-
billingAddress: { firstName: 'Jan', lastName: 'Novak', street: 'Hlavni 1', city: 'Praha', zip: '11000', country: 'CZ' },
|
|
280
|
-
});
|
|
281
|
-
}}>
|
|
282
|
-
{/* form fields */}
|
|
283
|
-
<button disabled={isCreating}>{isCreating ? 'Processing...' : 'Place order'}</button>
|
|
284
|
-
</form>
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
→ [Cart session management](#cart-session-management)
|
|
290
|
-
|
|
291
|
-
## 4. Customer authentication
|
|
292
|
-
|
|
293
|
-
Register, login, logout — with automatic token persistence and anonymous cart merge.
|
|
294
|
-
|
|
295
|
-
```tsx
|
|
296
|
-
'use client';
|
|
297
|
-
import { useAuth } from '@behio/storefront-sdk/react';
|
|
298
|
-
|
|
299
|
-
export function AuthSection() {
|
|
300
|
-
const { isLoggedIn, customer, login, logout, isLoggingIn, loginError } = useAuth();
|
|
301
|
-
|
|
302
|
-
if (isLoggedIn) {
|
|
303
|
-
return (
|
|
304
|
-
<>
|
|
305
|
-
<p>Welcome, {customer?.firstName || customer?.email}</p>
|
|
306
|
-
<button onClick={logout}>Logout</button>
|
|
307
|
-
</>
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return (
|
|
312
|
-
<form onSubmit={async (e) => {
|
|
313
|
-
e.preventDefault();
|
|
314
|
-
const fd = new FormData(e.currentTarget);
|
|
315
|
-
await login(fd.get('email') as string, fd.get('password') as string);
|
|
316
|
-
}}>
|
|
317
|
-
<input name="email" type="email" />
|
|
318
|
-
<input name="password" type="password" />
|
|
319
|
-
{loginError && <p>{loginError.message}</p>}
|
|
320
|
-
<button disabled={isLoggingIn}>Login</button>
|
|
321
|
-
</form>
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
Once logged in, authenticated hooks work automatically:
|
|
327
|
-
|
|
328
|
-
```tsx
|
|
329
|
-
import { useOrders, useCustomer, useAddresses } from '@behio/storefront-sdk/react';
|
|
330
|
-
|
|
331
|
-
const { data: orders } = useOrders();
|
|
332
|
-
const { data: profile, updateProfile } = useCustomer();
|
|
333
|
-
const { addresses, createAddress, deleteAddress } = useAddresses();
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
→ [Auto token refresh](#auto-token-refresh)
|
|
337
|
-
|
|
338
|
-
<a id="b-server-components--server-actions"></a>
|
|
339
|
-
## 5. Server Components & Server Actions (secure path)
|
|
340
|
-
|
|
341
|
-
For the **most secure** integration — API key stays on the server, tokens in httpOnly cookies, forms work without JavaScript (progressive enhancement).
|
|
342
|
-
|
|
343
|
-
**Server Component — data fetching:**
|
|
344
|
-
|
|
345
|
-
```tsx
|
|
346
|
-
// app/products/page.tsx (server component — no "use client")
|
|
347
|
-
import { BehioStorefront } from '@behio/storefront-sdk';
|
|
348
|
-
|
|
349
|
-
export default async function ProductsPage() {
|
|
350
|
-
const shop = new BehioStorefront({
|
|
351
|
-
apiKey: process.env.BEHIO_API_KEY!, // ← private, server-only
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
const products = await shop.catalog.getProducts({ limit: 24 });
|
|
355
|
-
|
|
356
|
-
return (
|
|
357
|
-
<div>
|
|
358
|
-
{products.items.map(p => (
|
|
359
|
-
<a key={p.id} href={`/products/${p.slug}`}>
|
|
360
|
-
<h3>{p.name}</h3>
|
|
361
|
-
<p>{p.price.amount} {p.price.currency}</p>
|
|
362
|
-
</a>
|
|
363
|
-
))}
|
|
364
|
-
</div>
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
**Server Actions — mutations:**
|
|
370
|
-
|
|
371
|
-
```tsx
|
|
372
|
-
// app/actions.ts
|
|
373
|
-
'use server';
|
|
374
|
-
import { BehioStorefront } from '@behio/storefront-sdk';
|
|
375
|
-
import { cookies } from 'next/headers';
|
|
376
|
-
|
|
377
|
-
function getShop() {
|
|
378
|
-
return new BehioStorefront({
|
|
379
|
-
apiKey: process.env.BEHIO_API_KEY!,
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export async function loginAction(_: unknown, formData: FormData) {
|
|
384
|
-
const shop = getShop();
|
|
385
|
-
try {
|
|
386
|
-
const result = await shop.auth.login({
|
|
387
|
-
email: formData.get('email') as string,
|
|
388
|
-
password: formData.get('password') as string,
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const cookieStore = await cookies();
|
|
392
|
-
cookieStore.set('behio_access', result.accessToken, {
|
|
393
|
-
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900,
|
|
394
|
-
});
|
|
395
|
-
cookieStore.set('behio_refresh', result.refreshToken, {
|
|
396
|
-
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 86400,
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
return { success: true, error: null };
|
|
400
|
-
} catch (err) {
|
|
401
|
-
return { success: false, error: err instanceof Error ? err.message : 'Login failed' };
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export async function addToCartAction(productId: string, quantity = 1) {
|
|
406
|
-
const shop = getShop();
|
|
407
|
-
const cookieStore = await cookies();
|
|
408
|
-
const cartSession = cookieStore.get('behio_cart_session')?.value;
|
|
409
|
-
if (cartSession) shop.setCartSession(cartSession);
|
|
410
|
-
|
|
411
|
-
const result = await shop.cart.addItem({ productId, quantity });
|
|
412
|
-
|
|
413
|
-
if (result.newSessionToken) {
|
|
414
|
-
cookieStore.set('behio_cart_session', result.newSessionToken, {
|
|
415
|
-
sameSite: 'lax', maxAge: 30 * 86400,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
return result;
|
|
419
|
-
}
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
```tsx
|
|
423
|
-
// app/auth/page.tsx
|
|
424
|
-
'use client';
|
|
425
|
-
import { useActionState } from 'react';
|
|
426
|
-
import { loginAction } from '../actions';
|
|
427
|
-
|
|
428
|
-
export default function LoginPage() {
|
|
429
|
-
const [state, formAction, isPending] = useActionState(loginAction, { success: false, error: null });
|
|
430
|
-
|
|
431
|
-
return (
|
|
432
|
-
<form action={formAction}>
|
|
433
|
-
<input name="email" type="email" placeholder="Email" required />
|
|
434
|
-
<input name="password" type="password" placeholder="Password" required />
|
|
435
|
-
{state.error && <p>{state.error}</p>}
|
|
436
|
-
<button disabled={isPending}>{isPending ? 'Logging in...' : 'Login'}</button>
|
|
437
|
-
</form>
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
---
|
|
443
|
-
|
|
444
|
-
# 🛠 Vanilla JS / Node.js path
|
|
445
|
-
|
|
446
|
-
For Node.js backends, serverless functions, CLI tools, Deno, Bun, or any non-React JavaScript environment.
|
|
447
|
-
|
|
448
|
-
## 1. Create a client
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
import { BehioStorefront } from '@behio/storefront-sdk';
|
|
452
|
-
|
|
453
|
-
const shop = new BehioStorefront({
|
|
454
|
-
apiKey: 'pk_live_xxx',
|
|
455
|
-
locale: 'cs', // optional
|
|
456
|
-
currency: 'CZK', // optional
|
|
457
|
-
timeout: 30000, // optional — request timeout (ms)
|
|
458
|
-
retries: 1, // optional — retry failed requests
|
|
459
|
-
});
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
## 2. Call any endpoint
|
|
463
|
-
|
|
464
|
-
Every API method returns a Promise. Types are fully auto-completed.
|
|
465
|
-
|
|
466
|
-
```typescript
|
|
467
|
-
// Catalog
|
|
468
|
-
const products = await shop.catalog.getProducts({
|
|
469
|
-
limit: 20,
|
|
470
|
-
sort: 'newest',
|
|
471
|
-
categories: ['electronics', 'books'], // array filters sent as repeated query params
|
|
472
|
-
hasDiscount: true,
|
|
25
|
+
const storefront = new BehioStorefront({
|
|
26
|
+
apiKey: 'pk_live_your_key',
|
|
473
27
|
});
|
|
474
|
-
const product = await shop.catalog.getProduct('my-product-slug');
|
|
475
|
-
const { categories } = await shop.catalog.getCategories();
|
|
476
28
|
|
|
477
|
-
//
|
|
478
|
-
const
|
|
479
|
-
await shop.auth.login({ email: '...', password: '...' });
|
|
29
|
+
// Fetch products
|
|
30
|
+
const { items } = await storefront.catalog.getProducts({ limit: 12 });
|
|
480
31
|
|
|
481
|
-
//
|
|
482
|
-
await
|
|
483
|
-
const cart = await shop.cart.get();
|
|
484
|
-
await shop.cart.applyDiscount('SAVE20');
|
|
32
|
+
// Add to cart
|
|
33
|
+
await storefront.cart.addItem({ productId: items[0].id, quantity: 1 });
|
|
485
34
|
|
|
486
35
|
// Checkout
|
|
487
|
-
const order = await
|
|
36
|
+
const order = await storefront.checkout.createOrder({
|
|
488
37
|
email: 'customer@example.com',
|
|
489
|
-
shippingAddress: {
|
|
490
|
-
billingAddress: { /* ... */ },
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// Orders (auth required)
|
|
494
|
-
const orders = await shop.orders.list();
|
|
495
|
-
const detail = await shop.orders.get('ORD-123');
|
|
496
|
-
|
|
497
|
-
// Public order tracking
|
|
498
|
-
const tracked = await shop.orders.track('tracking-token-uuid');
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
→ [All API methods](#all-api-methods)
|
|
502
|
-
|
|
503
|
-
---
|
|
504
|
-
|
|
505
|
-
# 📖 Reference
|
|
506
|
-
|
|
507
|
-
## All React hooks
|
|
508
|
-
|
|
509
|
-
| Hook | Purpose | Auth required |
|
|
510
|
-
|------|---------|---------------|
|
|
511
|
-
| [`useShopInfo()`](#useshopinfo) | E-shop info (name, currencies, languages) | No |
|
|
512
|
-
| [`useShopSeo(opts?)`](#useshopseo) | Per-locale SEO metadata for the homepage | No |
|
|
513
|
-
| [`useProducts(query?)`](#useproducts) | Product list with filters, pagination, search | No |
|
|
514
|
-
| [`useProduct(slug)`](#useproduct) | Product detail | No |
|
|
515
|
-
| [`useCategories(locale?)`](#usecategories) | Category tree | No |
|
|
516
|
-
| [`useLabels(locale?)`](#uselabels) | Product labels/tags | No |
|
|
517
|
-
| [`useFeatured()`](#usefeatured) | Featured products | No |
|
|
518
|
-
| [`useFilters()`](#usefilters) | Dynamic filter fields | No |
|
|
519
|
-
| [`useSearch(query)`](#usesearch) | Debounced product search | No |
|
|
520
|
-
| [`useCart()`](#usecart) | Cart state + actions | No |
|
|
521
|
-
| [`useCartCount()`](#usecartcount) | Cart item count (lightweight) | No |
|
|
522
|
-
| [`useAuth()`](#useauth) | Login, register, logout | No |
|
|
523
|
-
| [`useCheckout()`](#usecheckout) | Create order | No* |
|
|
524
|
-
| [`useOrders(opts?)`](#useorders) | Order list | Yes |
|
|
525
|
-
| [`useOrder(num)`](#useorder) | Order detail + cancel | Yes |
|
|
526
|
-
| [`useCustomer()`](#usecustomer) | Profile + update | Yes |
|
|
527
|
-
| [`useAddresses()`](#useaddresses) | Address CRUD | Yes |
|
|
528
|
-
| [`usePages()` / `usePage(slug)`](#usepages) | CMS pages | No |
|
|
529
|
-
| [`useBundles()` / `useBundle(slug)`](#usebundles) | Active bundles (sets) with auto-computed savings | No |
|
|
530
|
-
| [`useCrossSell(productSlug)`](#usecrosssell) | Related / upsell / cross-sell products per product | No |
|
|
531
|
-
| [`useProductPromotions(productSlug)`](#useproductpromotions) | Active promotions applicable to a product (with countdown) | No |
|
|
532
|
-
|
|
533
|
-
\* Guest checkout works without auth if the e-shop allows it.
|
|
534
|
-
|
|
535
|
-
### useShopInfo
|
|
536
|
-
```typescript
|
|
537
|
-
const { data, isLoading, error } = useShopInfo();
|
|
538
|
-
// data: ShopInfo
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
### useShopSeo
|
|
542
|
-
|
|
543
|
-
Returns per-locale SEO metadata (title, description, keywords, OG tags). Falls back to the shop's default locale when `locale` is omitted, and to `metaTitle / metaDescription` on the shop itself if there's no per-locale override.
|
|
544
|
-
|
|
545
|
-
```tsx
|
|
546
|
-
const { data: seo } = useShopSeo({ locale: 'cs' });
|
|
547
|
-
// data: { locale, title, description, keywords, ogTitle, ogDescription, ogImage }
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
**Server-side / SSR** — use the core client directly to fetch at request time and pass as `initialData`:
|
|
551
|
-
|
|
552
|
-
```tsx
|
|
553
|
-
// app/[locale]/page.tsx (Next.js RSC)
|
|
554
|
-
import { BehioStorefront } from '@behio/storefront-sdk';
|
|
555
|
-
import type { Metadata } from 'next';
|
|
556
|
-
|
|
557
|
-
const shop = new BehioStorefront({ apiKey: process.env.BEHIO_API_KEY! });
|
|
558
|
-
|
|
559
|
-
export async function generateMetadata({ params }: { params: { locale: string } }): Promise<Metadata> {
|
|
560
|
-
const seo = await shop.getShopSeo(params.locale);
|
|
561
|
-
return {
|
|
562
|
-
title: seo.title ?? undefined,
|
|
563
|
-
description: seo.description ?? undefined,
|
|
564
|
-
openGraph: {
|
|
565
|
-
title: seo.ogTitle ?? seo.title ?? undefined,
|
|
566
|
-
description: seo.ogDescription ?? seo.description ?? undefined,
|
|
567
|
-
images: seo.ogImage ? [seo.ogImage] : undefined,
|
|
568
|
-
},
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
export default async function Home({ params }: { params: { locale: string } }) {
|
|
573
|
-
const seo = await shop.getShopSeo(params.locale);
|
|
574
|
-
// Pass to client component for React Query hydration:
|
|
575
|
-
// <ClientHome initialSeo={seo} />
|
|
576
|
-
}
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
Client hydration:
|
|
580
|
-
```tsx
|
|
581
|
-
'use client';
|
|
582
|
-
import { useShopSeo } from '@behio/storefront-sdk/react';
|
|
583
|
-
|
|
584
|
-
export function ClientHome({ initialSeo }: { initialSeo: ShopSeo }) {
|
|
585
|
-
const { data } = useShopSeo({ locale: 'cs', initialData: initialSeo });
|
|
586
|
-
return <h1>{data.title}</h1>;
|
|
587
|
-
}
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### useProducts
|
|
591
|
-
|
|
592
|
-
Supports **both** traditional pagination and infinite scroll from one hook.
|
|
593
|
-
|
|
594
|
-
```typescript
|
|
595
|
-
const {
|
|
596
|
-
items, // flat array (all pages merged) — for infinite scroll
|
|
597
|
-
data, // PaginatedResponse (current page) — for traditional pagination
|
|
598
|
-
total, totalPages, currentPage, limit,
|
|
599
|
-
|
|
600
|
-
// Traditional pagination
|
|
601
|
-
page, setPage,
|
|
602
|
-
|
|
603
|
-
// Infinite scroll
|
|
604
|
-
loadMore, hasMore, isLoadingMore,
|
|
605
|
-
|
|
606
|
-
// State
|
|
607
|
-
isLoading, isFetching, error, isError, refetch,
|
|
608
|
-
} = useProducts({
|
|
609
|
-
page: 1,
|
|
610
|
-
limit: 24,
|
|
611
|
-
|
|
612
|
-
// Scalar filters
|
|
613
|
-
category: 'electronics', // single category slug
|
|
614
|
-
label: 'new', // single label slug
|
|
615
|
-
priceMin: 100,
|
|
616
|
-
priceMax: 5000,
|
|
617
|
-
currency: 'CZK',
|
|
618
|
-
locale: 'cs',
|
|
619
|
-
sort: 'price_asc', // or ProductSort.PRICE_ASC
|
|
620
|
-
inStock: true,
|
|
621
|
-
search: 'keyboard',
|
|
622
|
-
customFields: { material: 'aluminum' },
|
|
623
|
-
|
|
624
|
-
// Array filters — sent as repeated query params (?ids=a&ids=b)
|
|
625
|
-
ids: ['prod_1', 'prod_2'], // filter to specific IDs
|
|
626
|
-
slugs: ['red-shoes', 'blue-hat'], // filter to specific slugs
|
|
627
|
-
categories: ['electronics', 'books'], // OR logic (in ANY of these)
|
|
628
|
-
labels: ['sale', 'new'], // AND logic (must have ALL)
|
|
629
|
-
excludeIds: ['prod_999'], // exclude specific IDs (e.g. related products)
|
|
630
|
-
excludeCategories: ['archive'], // exclude products in these categories
|
|
631
|
-
|
|
632
|
-
// Boolean / time filters
|
|
633
|
-
hasDiscount: true, // only products with compareAtPrice
|
|
634
|
-
isFeatured: true, // only featured products
|
|
635
|
-
createdAfter: Date.now() - 7 * 864e5, // created in last 7 days (epoch ms)
|
|
636
|
-
|
|
637
|
-
// Hook options
|
|
638
|
-
enabled: true, // skip fetch until true
|
|
639
|
-
initialData: prefetchedData, // SSR hydration
|
|
38
|
+
shippingAddress: { firstName: 'Jan', lastName: 'Novak', street: 'Vodickova 12', city: 'Praha', zip: '11000', country: 'CZ' },
|
|
640
39
|
});
|
|
641
40
|
```
|
|
642
41
|
|
|
643
|
-
|
|
644
|
-
```tsx
|
|
645
|
-
const { items, loadMore, hasMore, isLoadingMore } = useProducts({ limit: 24 });
|
|
646
|
-
|
|
647
|
-
return (
|
|
648
|
-
<>
|
|
649
|
-
{items.map(p => <ProductCard key={p.id} product={p} />)}
|
|
650
|
-
{hasMore && <button onClick={loadMore} disabled={isLoadingMore}>Load more</button>}
|
|
651
|
-
</>
|
|
652
|
-
);
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
**Traditional pagination example:**
|
|
656
|
-
```tsx
|
|
657
|
-
const { data, page, setPage, totalPages } = useProducts({ limit: 24 });
|
|
658
|
-
|
|
659
|
-
return (
|
|
660
|
-
<>
|
|
661
|
-
{data?.items.map(p => <ProductCard key={p.id} product={p} />)}
|
|
662
|
-
<Pagination page={page} total={totalPages} onChange={setPage} />
|
|
663
|
-
</>
|
|
664
|
-
);
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
### useProduct
|
|
668
|
-
```typescript
|
|
669
|
-
const { data, isLoading, error } = useProduct('product-slug', {
|
|
670
|
-
locale: 'cs',
|
|
671
|
-
currency: 'CZK',
|
|
672
|
-
initialData: prefetchedData, // for SSR hydration
|
|
673
|
-
});
|
|
674
|
-
// data: ProductDetail
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
### useCategories
|
|
678
|
-
```typescript
|
|
679
|
-
const { data, isLoading } = useCategories('cs');
|
|
680
|
-
// data: Category[] (tree structure with children)
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
### useLabels
|
|
684
|
-
```typescript
|
|
685
|
-
const { data } = useLabels('cs');
|
|
686
|
-
// data: ProductLabel[]
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
### useFeatured
|
|
690
|
-
```typescript
|
|
691
|
-
const { data } = useFeatured();
|
|
692
|
-
// data: PaginatedResponse<ProductListItem>
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
### useFilters
|
|
696
|
-
```typescript
|
|
697
|
-
const { data } = useFilters();
|
|
698
|
-
// data: FilterField[] — use to build dynamic filter UI
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
### useSearch
|
|
702
|
-
```typescript
|
|
703
|
-
const { data, isLoading } = useSearch(query, { debounceMs: 300, limit: 10 });
|
|
704
|
-
// Debounced — only fetches after user stops typing
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
### useCart
|
|
708
|
-
```typescript
|
|
709
|
-
const {
|
|
710
|
-
cart, // Cart | null
|
|
711
|
-
isLoading, error,
|
|
712
|
-
isEmpty, // boolean
|
|
713
|
-
itemCount, // number
|
|
714
|
-
// Actions
|
|
715
|
-
addItem, // (productId, quantity?) => Promise
|
|
716
|
-
updateQuantity, // (itemId, quantity) => Promise
|
|
717
|
-
removeItem, // (itemId) => Promise
|
|
718
|
-
clear, // () => Promise
|
|
719
|
-
applyDiscount, // (code) => Promise
|
|
720
|
-
removeDiscount, // () => Promise
|
|
721
|
-
merge, // () => Promise — merges anonymous cart after login
|
|
722
|
-
// States
|
|
723
|
-
isAdding, isUpdating, isRemoving,
|
|
724
|
-
} = useCart();
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
### useCartCount
|
|
728
|
-
```typescript
|
|
729
|
-
const count = useCartCount();
|
|
730
|
-
// number — lightweight, reads from cache first
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
### useAuth
|
|
734
|
-
```typescript
|
|
735
|
-
const {
|
|
736
|
-
isLoggedIn, customer,
|
|
737
|
-
login, // (email, password) => Promise
|
|
738
|
-
register, // (input) => Promise
|
|
739
|
-
logout, // () => Promise
|
|
740
|
-
forgotPassword, // (email) => Promise
|
|
741
|
-
resetPassword, // (token, newPassword) => Promise
|
|
742
|
-
verifyEmail, // (token) => Promise
|
|
743
|
-
isLoggingIn, isRegistering,
|
|
744
|
-
loginError, registerError,
|
|
745
|
-
} = useAuth();
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
### useCheckout
|
|
749
|
-
```typescript
|
|
750
|
-
const {
|
|
751
|
-
createOrder, // (input) => Promise<OrderDetail>
|
|
752
|
-
isCreating, error,
|
|
753
|
-
order, // OrderDetail | null — last created order
|
|
754
|
-
reset, // () => void
|
|
755
|
-
} = useCheckout();
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
### useOrders
|
|
759
|
-
```typescript
|
|
760
|
-
const { data, isLoading } = useOrders({ page: 1, limit: 10 });
|
|
761
|
-
// data: PaginatedResponse<OrderListItem>
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
### useOrder
|
|
765
|
-
```typescript
|
|
766
|
-
const { data, cancel, isCancelling } = useOrder('ORD-123');
|
|
767
|
-
// data: OrderDetail
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
### useCustomer
|
|
771
|
-
```typescript
|
|
772
|
-
const { data, updateProfile, isUpdating } = useCustomer();
|
|
773
|
-
// data: CustomerProfile
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
### useAddresses
|
|
777
|
-
```typescript
|
|
778
|
-
const {
|
|
779
|
-
addresses,
|
|
780
|
-
createAddress, updateAddress, deleteAddress,
|
|
781
|
-
isCreating, isDeleting,
|
|
782
|
-
} = useAddresses();
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
### usePages
|
|
786
|
-
```typescript
|
|
787
|
-
const { data: pages } = usePages('cs');
|
|
788
|
-
const { data: page } = usePage('about-us', 'cs');
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
### useBundles / useBundle
|
|
792
|
-
|
|
793
|
-
**What it is.** A "bundle" is a merchant-curated set of products sold together
|
|
794
|
-
at a single fixed price that's (usually) lower than buying the components
|
|
795
|
-
individually — think *starter kit*, *holiday gift set*, *3-for-2 deals*, or
|
|
796
|
-
*"breakfast combo"*. The merchant defines what goes in (which products, what
|
|
797
|
-
quantities, what cover image) and sets one total price for the whole thing.
|
|
798
|
-
|
|
799
|
-
**Why it matters.** Bundles are one of the highest-ROI features in e-commerce:
|
|
800
|
-
they raise average order value without having to discount individual products,
|
|
801
|
-
they give customers a clear "good deal" signal (the savings badge), and they
|
|
802
|
-
let you clear slow-moving inventory by pairing it with fast-movers. Most eshop
|
|
803
|
-
platforms treat bundles as paid add-ons or plugins — here it's native.
|
|
804
|
-
|
|
805
|
-
**Where you use it.**
|
|
806
|
-
|
|
807
|
-
- **Homepage / landing pages** — list active bundles as hero cards with cover
|
|
808
|
-
image + `-25%` badge. Drives impulse purchase.
|
|
809
|
-
- **Category pages** — show a relevant bundle ("Everything for grilling") next
|
|
810
|
-
to individual products in the same category.
|
|
811
|
-
- **Cart / checkout** — suggest a bundle as upsell ("Add these two more items
|
|
812
|
-
and get the whole set with a 20% discount").
|
|
813
|
-
- **Dedicated `/bundles` page** — marketing landing for all active sets.
|
|
814
|
-
|
|
815
|
-
**What's in the data:**
|
|
816
|
-
|
|
817
|
-
- `bundlePrice` — what the customer pays for the whole set
|
|
818
|
-
- `itemsSum` — what the components would cost individually (sum of default prices)
|
|
819
|
-
- `savings` — `itemsSum - bundlePrice` (absolute savings in the currency)
|
|
820
|
-
- `savingsPercent` — pre-computed percent so you don't have to do the math in JSX
|
|
821
|
-
- `items[]` — the components with their quantities (for display and stock check)
|
|
822
|
-
- `endsAt` — optional expiry timestamp; if set, the bundle auto-deactivates
|
|
42
|
+
## React Hooks
|
|
823
43
|
|
|
824
44
|
```tsx
|
|
825
|
-
import {
|
|
826
|
-
|
|
827
|
-
// ---- Homepage hero: all active bundles ----
|
|
828
|
-
function BundlesGrid() {
|
|
829
|
-
const { data, isLoading } = useBundles();
|
|
830
|
-
if (isLoading) return <Skeleton />;
|
|
831
|
-
if (!data?.items.length) return null; // no active bundles → hide section
|
|
45
|
+
import { BehioProvider, useProducts, useCart, useAddToCart } from '@behio/storefront-sdk/react';
|
|
832
46
|
|
|
47
|
+
function App() {
|
|
833
48
|
return (
|
|
834
|
-
<
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
{bundle.coverImage && <img src={bundle.coverImage} alt={bundle.name} />}
|
|
838
|
-
<h3>{bundle.name}</h3>
|
|
839
|
-
<div>
|
|
840
|
-
<strong>{bundle.bundlePrice} {bundle.currency}</strong>
|
|
841
|
-
{bundle.savings > 0 && (
|
|
842
|
-
<>
|
|
843
|
-
<s>{bundle.itemsSum} {bundle.currency}</s>
|
|
844
|
-
<span className="badge">-{bundle.savingsPercent}%</span>
|
|
845
|
-
</>
|
|
846
|
-
)}
|
|
847
|
-
</div>
|
|
848
|
-
<p>{bundle.items.length} products · save {bundle.savings} {bundle.currency}</p>
|
|
849
|
-
</a>
|
|
850
|
-
))}
|
|
851
|
-
</section>
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// ---- Bundle detail page with "Add to cart" ----
|
|
856
|
-
function BundleDetail({ slug }: { slug: string }) {
|
|
857
|
-
const { data: bundle, isLoading } = useBundle(slug);
|
|
858
|
-
const { client } = useBehio();
|
|
859
|
-
const { refresh } = useCart();
|
|
860
|
-
|
|
861
|
-
if (isLoading) return <Skeleton />;
|
|
862
|
-
if (!bundle) return <NotFound />;
|
|
863
|
-
|
|
864
|
-
async function addToCart() {
|
|
865
|
-
await client.cart.addBundle(bundle.id, 1);
|
|
866
|
-
await refresh(); // cart badge updates everywhere
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
return (
|
|
870
|
-
<article>
|
|
871
|
-
<h1>{bundle.name}</h1>
|
|
872
|
-
<p>{bundle.description}</p>
|
|
873
|
-
|
|
874
|
-
<ul>
|
|
875
|
-
{bundle.items.map((item) => (
|
|
876
|
-
<li key={item.productId}>
|
|
877
|
-
{item.quantity}× {item.name}
|
|
878
|
-
{item.defaultPrice && (
|
|
879
|
-
<span className="text-muted">
|
|
880
|
-
({item.defaultPrice} {bundle.currency} /pc regular price)
|
|
881
|
-
</span>
|
|
882
|
-
)}
|
|
883
|
-
</li>
|
|
884
|
-
))}
|
|
885
|
-
</ul>
|
|
886
|
-
|
|
887
|
-
<div className="price-box">
|
|
888
|
-
<strong>{bundle.bundlePrice} {bundle.currency}</strong>
|
|
889
|
-
{bundle.savings > 0 && (
|
|
890
|
-
<p>
|
|
891
|
-
Individually it would cost <s>{bundle.itemsSum} {bundle.currency}</s> —
|
|
892
|
-
you save <strong>{bundle.savings} {bundle.currency}</strong>
|
|
893
|
-
({bundle.savingsPercent}%)
|
|
894
|
-
</p>
|
|
895
|
-
)}
|
|
896
|
-
<button onClick={addToCart}>Add the whole bundle to cart</button>
|
|
897
|
-
</div>
|
|
898
|
-
</article>
|
|
899
|
-
);
|
|
900
|
-
}
|
|
901
|
-
```
|
|
902
|
-
|
|
903
|
-
**Behind the scenes at checkout.** When the customer buys a bundle, Behio
|
|
904
|
-
splits it into individual order items with prices distributed proportionally
|
|
905
|
-
based on each component's default price (this is kept so accounting and
|
|
906
|
-
stock decrements work correctly). The invoice total matches `bundlePrice` —
|
|
907
|
-
the customer sees one line item, inventory is decremented component-by-component.
|
|
908
|
-
|
|
909
|
-
---
|
|
910
|
-
|
|
911
|
-
### useCrossSell
|
|
912
|
-
|
|
913
|
-
**What it is.** Three related lists of product recommendations shown on a
|
|
914
|
-
product detail page:
|
|
915
|
-
|
|
916
|
-
- **Related** — alternatives for the same need (another leash, another dog
|
|
917
|
-
food). "If this one caught your eye, here are similar options in the same
|
|
918
|
-
category."
|
|
919
|
-
- **Upsell** — a better / more premium version. "Looking at the basic collar?
|
|
920
|
-
Here's the leather-stitched premium version for 2× the price." Goal: raise
|
|
921
|
-
average order value by steering to a better margin.
|
|
922
|
-
- **Cross-sell** — complementary products. Leash for the collar, cleaner for
|
|
923
|
-
the bowl. Pure AOV boost on PDP and in cart.
|
|
924
|
-
|
|
925
|
-
**Why split them.** Each type has a different UX role and should be phrased
|
|
926
|
-
differently. Mixed together they lose context — the customer can't tell why
|
|
927
|
-
they're being shown.
|
|
928
|
-
|
|
929
|
-
**Where to use it.**
|
|
930
|
-
|
|
931
|
-
- **Product detail page** — three separate sections below the description
|
|
932
|
-
(or in a sidebar column). Highest conversion impact is **Cross-sell
|
|
933
|
-
"Frequently bought together"** right next to the "Add to cart" button.
|
|
934
|
-
- **Cart sidebar** — mini cross-sell widget ("Don't forget these too").
|
|
935
|
-
- **Post-purchase page** — "Want to add these?" for a follow-up order.
|
|
936
|
-
|
|
937
|
-
**Note.** The endpoint returns **only active products from the same eshop**.
|
|
938
|
-
If you linked a product in admin and later disabled or deleted it, it drops
|
|
939
|
-
from the list automatically — no need to handle that on the frontend.
|
|
940
|
-
|
|
941
|
-
```tsx
|
|
942
|
-
import { useCrossSell, useBehio } from '@behio/storefront-sdk/react';
|
|
943
|
-
|
|
944
|
-
function CrossSellSection({ title, items }: { title: string; items: CrossSellItem[] }) {
|
|
945
|
-
if (!items?.length) return null; // hide empty section
|
|
946
|
-
return (
|
|
947
|
-
<section>
|
|
948
|
-
<h2>{title}</h2>
|
|
949
|
-
<div className="carousel">
|
|
950
|
-
{items.map((item) => (
|
|
951
|
-
<a key={item.productId} href={`/product/${item.slug}`} className="card">
|
|
952
|
-
{item.imageUrl && <img src={item.imageUrl} alt={item.name} />}
|
|
953
|
-
<h4>{item.name}</h4>
|
|
954
|
-
<span>{item.price}</span>
|
|
955
|
-
{item.stockCached === 0 && <span className="text-red">Sold out</span>}
|
|
956
|
-
</a>
|
|
957
|
-
))}
|
|
958
|
-
</div>
|
|
959
|
-
</section>
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function ProductDetail({ slug }: { slug: string }) {
|
|
964
|
-
const { data: crossSell } = useCrossSell(slug);
|
|
965
|
-
|
|
966
|
-
return (
|
|
967
|
-
<>
|
|
968
|
-
{/* ... product info ... */}
|
|
969
|
-
|
|
970
|
-
<CrossSellSection
|
|
971
|
-
title="Frequently bought together"
|
|
972
|
-
items={crossSell?.crossSell ?? []}
|
|
973
|
-
/>
|
|
974
|
-
<CrossSellSection
|
|
975
|
-
title="You might also like"
|
|
976
|
-
items={crossSell?.related ?? []}
|
|
977
|
-
/>
|
|
978
|
-
<CrossSellSection
|
|
979
|
-
title="Want a premium version?"
|
|
980
|
-
items={crossSell?.upsell ?? []}
|
|
981
|
-
/>
|
|
982
|
-
</>
|
|
49
|
+
<BehioProvider client={storefront}>
|
|
50
|
+
<ProductList />
|
|
51
|
+
</BehioProvider>
|
|
983
52
|
);
|
|
984
53
|
}
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
**Tip — merge into a single section.** If you have sparse data and want
|
|
988
|
-
to keep it simple, combine all three into one list:
|
|
989
|
-
|
|
990
|
-
```tsx
|
|
991
|
-
const allRecommendations = [
|
|
992
|
-
...(crossSell?.crossSell ?? []),
|
|
993
|
-
...(crossSell?.related ?? []),
|
|
994
|
-
...(crossSell?.upsell ?? []),
|
|
995
|
-
].slice(0, 6);
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
---
|
|
999
|
-
|
|
1000
|
-
### useProductPromotions
|
|
1001
|
-
|
|
1002
|
-
**What it is.** Returns the currently-active promotions (`Eshop_Promotion`)
|
|
1003
|
-
applicable to a given product — i.e. all promotions where
|
|
1004
|
-
`startsAt <= now < endsAt` and the product matches the promotion scope
|
|
1005
|
-
(`ALL_PRODUCTS`, `SPECIFIC_PRODUCTS`, `CATEGORIES`, or `LABELS`; the backend
|
|
1006
|
-
resolves it for you).
|
|
1007
|
-
|
|
1008
|
-
**What it is NOT.** It's not a computation of the *final discounted price* —
|
|
1009
|
-
that's a separate layer (the cart evaluator). This hook is purely for the
|
|
1010
|
-
**display layer**: badges, countdowns, "sale price until midnight" banners.
|
|
1011
54
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
(Amazon Lightning Deals, Booking.com "3 people booked in the last 24h",
|
|
1016
|
-
etc.). The hook delivers the data; you build the countdown component.
|
|
55
|
+
function ProductList() {
|
|
56
|
+
const { data, isLoading } = useProducts({ limit: 12 });
|
|
57
|
+
const addToCart = useAddToCart();
|
|
1017
58
|
|
|
1018
|
-
|
|
59
|
+
if (isLoading) return <div>Loading...</div>;
|
|
1019
60
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
**Fields returned:**
|
|
1027
|
-
|
|
1028
|
-
- `id`, `name`, `slug` — promotion identity
|
|
1029
|
-
- `type` — `FLASH_SALE` / `SEASONAL` / `CLEARANCE` / `BOGO` / `BUNDLE` / `LOYALTY`
|
|
1030
|
-
- `discountType` — `PERCENTAGE` / `FIXED_AMOUNT` / `FREE_SHIPPING` / `BUY_X_GET_Y`
|
|
1031
|
-
- `discountValue` — the value (% for PERCENTAGE, currency amount for FIXED_AMOUNT, …)
|
|
1032
|
-
- `endsAt` — end timestamp. If `null`, the promotion runs indefinitely.
|
|
1033
|
-
- `badgeText`, `badgeColor` — merchant-set badge text and color (e.g. "-30%"
|
|
1034
|
-
in red). If both are `null`, fall back to a default derived from `discountType`.
|
|
1035
|
-
- `showCountdown` — **respect this!** If the merchant opted out of showing
|
|
1036
|
-
a countdown, don't render one. Not all promotions (CLEARANCE, LOYALTY)
|
|
1037
|
-
make sense to count down.
|
|
1038
|
-
- `couponRequired` — the promotion applies only after the customer enters a
|
|
1039
|
-
code at checkout. On the PDP show an info box ("Use code SUMMER at
|
|
1040
|
-
checkout"); don't present it as an automatic discount.
|
|
1041
|
-
|
|
1042
|
-
```tsx
|
|
1043
|
-
import { useProductPromotions } from '@behio/storefront-sdk/react';
|
|
1044
|
-
import { useEffect, useState } from 'react';
|
|
1045
|
-
|
|
1046
|
-
function ProductPromotionBanner({ slug }: { slug: string }) {
|
|
1047
|
-
// Refetch every 4 minutes so the promotion list updates when one ends
|
|
1048
|
-
// and another begins. The countdown itself doesn't need a refetch —
|
|
1049
|
-
// it ticks locally off `endsAt`.
|
|
1050
|
-
const { data } = useProductPromotions(slug, { refetchIntervalMs: 4 * 60_000 });
|
|
1051
|
-
const promotions = data?.items ?? [];
|
|
1052
|
-
|
|
1053
|
-
if (!promotions.length) return null;
|
|
1054
|
-
|
|
1055
|
-
// Highest-priority promotion (the BE already sorts by priority DESC,
|
|
1056
|
-
// then createdAt ASC).
|
|
1057
|
-
const promo = promotions[0];
|
|
1058
|
-
|
|
1059
|
-
const badge = promo.badgeText ?? formatDefaultBadge(promo);
|
|
1060
|
-
const badgeColor = promo.badgeColor ?? '#ef4444';
|
|
1061
|
-
|
|
1062
|
-
return (
|
|
1063
|
-
<div className="promo-banner" style={{ backgroundColor: badgeColor + '20', borderColor: badgeColor }}>
|
|
1064
|
-
<div>
|
|
1065
|
-
<span className="badge" style={{ backgroundColor: badgeColor }}>{badge}</span>
|
|
1066
|
-
<strong>{promo.name}</strong>
|
|
1067
|
-
{promo.couponRequired && (
|
|
1068
|
-
<p>Use code <code>{promo.name}</code> at checkout to redeem</p>
|
|
1069
|
-
)}
|
|
1070
|
-
</div>
|
|
1071
|
-
{promo.showCountdown && promo.endsAt && <Countdown endsAt={promo.endsAt} />}
|
|
61
|
+
return data.items.map(p => (
|
|
62
|
+
<div key={p.id}>
|
|
63
|
+
<h3>{p.name} — {p.price} {p.currency}</h3>
|
|
64
|
+
<button onClick={() => addToCart.mutateAsync({ productId: p.id, quantity: 1 })}>
|
|
65
|
+
Add to Cart
|
|
66
|
+
</button>
|
|
1072
67
|
</div>
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function Countdown({ endsAt }: { endsAt: number }) {
|
|
1077
|
-
const [now, setNow] = useState(Date.now());
|
|
1078
|
-
useEffect(() => {
|
|
1079
|
-
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
1080
|
-
return () => clearInterval(id);
|
|
1081
|
-
}, []);
|
|
1082
|
-
|
|
1083
|
-
const ms = Math.max(0, endsAt - now);
|
|
1084
|
-
if (ms === 0) return <span>Sale ended</span>;
|
|
1085
|
-
|
|
1086
|
-
const d = Math.floor(ms / 86400_000);
|
|
1087
|
-
const h = Math.floor((ms % 86400_000) / 3600_000);
|
|
1088
|
-
const m = Math.floor((ms % 3600_000) / 60_000);
|
|
1089
|
-
const s = Math.floor((ms % 60_000) / 1000);
|
|
1090
|
-
|
|
1091
|
-
if (d > 0) return <span>Ends in {d}d {h}h {m}m</span>;
|
|
1092
|
-
return <span>Ends in {h}h {m}m {s}s</span>;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
function formatDefaultBadge(p: { discountType: string; discountValue: number }) {
|
|
1096
|
-
switch (p.discountType) {
|
|
1097
|
-
case 'PERCENTAGE': return `-${p.discountValue}%`;
|
|
1098
|
-
case 'FIXED_AMOUNT': return `-${p.discountValue}`;
|
|
1099
|
-
case 'FREE_SHIPPING': return 'Free shipping';
|
|
1100
|
-
case 'BUY_X_GET_Y': return `${p.discountValue}+1 FREE`;
|
|
1101
|
-
default: return 'SALE';
|
|
1102
|
-
}
|
|
68
|
+
));
|
|
1103
69
|
}
|
|
1104
70
|
```
|
|
1105
71
|
|
|
1106
|
-
|
|
1107
|
-
product card in a list (no countdown), call the hook per card and pick the
|
|
1108
|
-
first promo:
|
|
72
|
+
## What's Included
|
|
1109
73
|
|
|
1110
|
-
|
|
1111
|
-
function ProductCardBadge({ slug }: { slug: string }) {
|
|
1112
|
-
const { data } = useProductPromotions(slug);
|
|
1113
|
-
const promo = data?.items[0];
|
|
1114
|
-
if (!promo) return null;
|
|
1115
|
-
return (
|
|
1116
|
-
<span className="badge" style={{ background: promo.badgeColor ?? '#ef4444' }}>
|
|
1117
|
-
{promo.badgeText ?? formatDefaultBadge(promo)}
|
|
1118
|
-
</span>
|
|
1119
|
-
);
|
|
1120
|
-
}
|
|
1121
|
-
```
|
|
74
|
+
### SDK Modules
|
|
1122
75
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
76
|
+
| Module | Description |
|
|
77
|
+
|--------|-------------|
|
|
78
|
+
| `catalog` | Products, categories, labels, search, filters, bundles, cross-sell, promotions |
|
|
79
|
+
| `auth` | Register, login, logout, password reset, token refresh |
|
|
80
|
+
| `cart` | Items, discounts, gift cards, bundles, cart merge |
|
|
81
|
+
| `checkout` | Create orders with atomic stock/payment validation |
|
|
82
|
+
| `orders` | List, detail, tracking, cancel |
|
|
83
|
+
| `customer` | Profile, addresses, password change |
|
|
84
|
+
| `wishlist` | Add, remove, check |
|
|
85
|
+
| `reviews` | Submit, list, vote helpful |
|
|
86
|
+
| `addresses` | Google Places autocomplete with debounce hook |
|
|
87
|
+
| `returns` | Submit return requests |
|
|
88
|
+
| `consent` | Cookie consent (GDPR) |
|
|
89
|
+
| `quotes` | B2B quote requests |
|
|
90
|
+
| `pages` | CMS pages |
|
|
1126
91
|
|
|
1127
|
-
|
|
92
|
+
### React Hooks (30+)
|
|
1128
93
|
|
|
1129
|
-
|
|
1130
|
-
```typescript
|
|
1131
|
-
shop.catalog.getProducts(query?) → PaginatedResponse<ProductListItem>
|
|
1132
|
-
shop.catalog.getProduct(slug) → ProductDetail
|
|
1133
|
-
shop.catalog.getCategories(locale?) → { categories: Category[] }
|
|
1134
|
-
shop.catalog.getCategory(slug, locale?) → CategoryDetail
|
|
1135
|
-
shop.catalog.getCategoryProducts(slug, q?) → PaginatedResponse<ProductListItem>
|
|
1136
|
-
shop.catalog.getLabels(locale?) → { labels: ProductLabel[] }
|
|
1137
|
-
shop.catalog.getFeatured() → PaginatedResponse<ProductListItem>
|
|
1138
|
-
shop.catalog.getFilters() → { filters: FilterField[] }
|
|
1139
|
-
shop.catalog.search(query, opts?) → PaginatedResponse<ProductListItem>
|
|
1140
|
-
shop.catalog.getBundles() → { items: Bundle[] }
|
|
1141
|
-
shop.catalog.getBundle(slug) → Bundle
|
|
1142
|
-
shop.catalog.getCrossSell(productSlug) → { related, upsell, crossSell: CrossSellItem[] }
|
|
1143
|
-
shop.catalog.getProductPromotions(slug) → { items: ActivePromotion[] }
|
|
1144
|
-
```
|
|
94
|
+
`useProducts` · `useProduct` · `useCategories` · `useCategoryProducts` · `useFeaturedProducts` · `useLabels` · `useProductSearch` · `useFilters` · `useBundles` · `useBundle` · `useCrossSell` · `useProductPromotions` · `useGiftCardBalance` · `useCart` · `useAddToCart` · `useUpdateCartItem` · `useRemoveCartItem` · `useCheckout` · `useOrders` · `useOrder` · `useOrderTracking` · `useCustomerProfile` · `useAddresses` · `useAddressAutocomplete` · `useWishlist` · `useProductReviews` · `useSubmitReview` · `useShopInfo` · `useShopSeo` · `useCartCount`
|
|
1145
95
|
|
|
1146
|
-
###
|
|
1147
|
-
```typescript
|
|
1148
|
-
shop.cart.addBundle(bundleId, quantity?) → Cart
|
|
1149
|
-
shop.cart.updateBundleQuantity(id, qty) → Cart
|
|
1150
|
-
shop.cart.removeBundle(bundleId) → Cart
|
|
1151
|
-
```
|
|
1152
|
-
|
|
1153
|
-
### Auth
|
|
1154
|
-
```typescript
|
|
1155
|
-
shop.auth.register(input) → AuthTokens
|
|
1156
|
-
shop.auth.login(input) → AuthTokens
|
|
1157
|
-
shop.auth.refresh(token?) → AuthTokens
|
|
1158
|
-
shop.auth.logout(token?) → MessageResponse
|
|
1159
|
-
shop.auth.forgotPassword(email) → MessageResponse
|
|
1160
|
-
shop.auth.resetPassword(token, password) → MessageResponse
|
|
1161
|
-
shop.auth.verifyEmail(token) → MessageResponse
|
|
1162
|
-
shop.auth.isLoggedIn() → boolean
|
|
1163
|
-
```
|
|
96
|
+
### Framework Support
|
|
1164
97
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
shop.cart.removeItem(itemId) → Cart
|
|
1171
|
-
shop.cart.clear() → void
|
|
1172
|
-
shop.cart.merge() → Cart
|
|
1173
|
-
shop.cart.applyDiscount(code) → Cart
|
|
1174
|
-
shop.cart.removeDiscount() → Cart
|
|
1175
|
-
```
|
|
98
|
+
- **Next.js** — Server components + client hooks, SSR ready
|
|
99
|
+
- **React + Vite** — Standard SPA setup
|
|
100
|
+
- **Nuxt 3** — Composables with SSR
|
|
101
|
+
- **Vue + Vite** — Provide/inject pattern
|
|
102
|
+
- **Vanilla JS** — Works in any runtime (Node.js, Deno, Bun, Cloudflare Workers)
|
|
1176
103
|
|
|
1177
|
-
###
|
|
1178
|
-
```typescript
|
|
1179
|
-
shop.checkout.createOrder(input) → OrderDetail
|
|
1180
|
-
```
|
|
104
|
+
### Built-in Features
|
|
1181
105
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
```
|
|
106
|
+
- Automatic JWT token refresh on 401
|
|
107
|
+
- Configurable retry with backoff (5xx, 429)
|
|
108
|
+
- Rate limit tracking and warnings
|
|
109
|
+
- Request/response interceptors
|
|
110
|
+
- Event system (auth, cart, order lifecycle)
|
|
111
|
+
- Cart session persistence
|
|
1189
112
|
|
|
1190
|
-
|
|
1191
|
-
```typescript
|
|
1192
|
-
shop.customer.getProfile() → CustomerProfile
|
|
1193
|
-
shop.customer.updateProfile(data) → CustomerProfile
|
|
1194
|
-
shop.customer.changePassword(current, new) → MessageResponse
|
|
1195
|
-
shop.customer.getAddresses() → { items: CustomerAddress[] }
|
|
1196
|
-
shop.customer.createAddress(data) → CustomerAddress
|
|
1197
|
-
shop.customer.updateAddress(id, data) → CustomerAddress
|
|
1198
|
-
shop.customer.deleteAddress(id) → void
|
|
1199
|
-
```
|
|
1200
|
-
|
|
1201
|
-
### Pages
|
|
1202
|
-
```typescript
|
|
1203
|
-
shop.pages.list(locale?) → { pages: Page[] }
|
|
1204
|
-
shop.pages.get(slug, locale?) → PageDetail
|
|
1205
|
-
```
|
|
1206
|
-
|
|
1207
|
-
### Instance utilities
|
|
1208
|
-
```typescript
|
|
1209
|
-
shop.getShopInfo() → ShopInfo
|
|
1210
|
-
shop.getShopSeo(locale?) → ShopSeo (per-locale SEO for SSR/metadata)
|
|
1211
|
-
shop.setTokens({ accessToken, refreshToken })
|
|
1212
|
-
shop.clearTokens()
|
|
1213
|
-
shop.getAccessToken() → string | undefined
|
|
1214
|
-
shop.getRefreshToken() → string | undefined
|
|
1215
|
-
shop.setCartSession(token)
|
|
1216
|
-
shop.getCartSession() → string | undefined
|
|
1217
|
-
shop.clearCartSession()
|
|
1218
|
-
shop.on(event, handler) → unsubscribe fn
|
|
1219
|
-
shop.addRequestInterceptor(fn) → unsubscribe fn
|
|
1220
|
-
shop.addResponseInterceptor(fn) → unsubscribe fn
|
|
1221
|
-
shop.getRateLimitInfo() → { remaining, reset }
|
|
1222
|
-
```
|
|
113
|
+
## Documentation
|
|
1223
114
|
|
|
1224
|
-
|
|
115
|
+
Full API reference, framework guides, and examples:
|
|
1225
116
|
|
|
1226
|
-
|
|
1227
|
-
ProductListItem {
|
|
1228
|
-
id, slug, name, shortDescription?, sku, gtin?,
|
|
1229
|
-
price: ProductPrice,
|
|
1230
|
-
inStock, stockQuantity?, image?,
|
|
1231
|
-
labels: ProductLabel[], isFeatured
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
ProductDetail extends ProductListItem {
|
|
1235
|
-
longDescription?,
|
|
1236
|
-
images[], categories[], variants[],
|
|
1237
|
-
volumePricing[], customFields, seo,
|
|
1238
|
-
weight?, weightUnit?,
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
ProductPrice { amount: number, currency: string, compareAtPrice?: number | null }
|
|
1242
|
-
|
|
1243
|
-
Cart {
|
|
1244
|
-
id, sessionToken?,
|
|
1245
|
-
items: CartItem[],
|
|
1246
|
-
subtotal, discountTotal, discount?,
|
|
1247
|
-
grandTotal, currency, itemCount
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
CartItem {
|
|
1251
|
-
id, product: CartItemProduct,
|
|
1252
|
-
quantity, unitPrice, totalPrice,
|
|
1253
|
-
priceChanged, volumePriceApplied
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
OrderDetail {
|
|
1257
|
-
orderNumber, status, paymentStatus, fulfillmentStatus,
|
|
1258
|
-
items[], shippingAddress, billingAddress,
|
|
1259
|
-
subtotal, taxTotal, shippingTotal, discountTotal, grandTotal, currency,
|
|
1260
|
-
statusHistory[], trackingToken?
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
CustomerProfile {
|
|
1264
|
-
id, email, firstName?, lastName?, phone?, emailVerified
|
|
1265
|
-
}
|
|
1266
|
-
```
|
|
1267
|
-
|
|
1268
|
-
## Constants / Enums
|
|
1269
|
-
|
|
1270
|
-
Type-safe constants for common values — autocomplete, iterable, zero runtime cost.
|
|
1271
|
-
|
|
1272
|
-
```typescript
|
|
1273
|
-
import {
|
|
1274
|
-
ProductSort,
|
|
1275
|
-
OrderStatuses,
|
|
1276
|
-
PaymentStatuses,
|
|
1277
|
-
FulfillmentStatuses,
|
|
1278
|
-
AddressTypes,
|
|
1279
|
-
} from '@behio/storefront-sdk';
|
|
1280
|
-
|
|
1281
|
-
// Use in queries
|
|
1282
|
-
shop.catalog.getProducts({ sort: ProductSort.PRICE_ASC });
|
|
1283
|
-
|
|
1284
|
-
// Type-safe comparisons
|
|
1285
|
-
if (order.status === OrderStatuses.SHIPPED) { /* ... */ }
|
|
1286
|
-
if (order.paymentStatus === PaymentStatuses.PAID) { /* ... */ }
|
|
1287
|
-
|
|
1288
|
-
// Iterate for dropdowns
|
|
1289
|
-
Object.values(ProductSort); // ['price_asc', 'price_desc', ...]
|
|
1290
|
-
Object.values(OrderStatuses); // ['PENDING', 'CONFIRMED', ...]
|
|
1291
|
-
```
|
|
1292
|
-
|
|
1293
|
-
Available constants:
|
|
1294
|
-
- `ProductSort`: `PRICE_ASC`, `PRICE_DESC`, `NAME_ASC`, `NAME_DESC`, `NEWEST`, `FEATURED`
|
|
1295
|
-
- `OrderStatuses`: `PENDING`, `CONFIRMED`, `PROCESSING`, `SHIPPED`, `DELIVERED`, `CANCELLED`, `REFUNDED`
|
|
1296
|
-
- `PaymentStatuses`: `UNPAID`, `PAID`, `PARTIALLY_REFUNDED`, `REFUNDED`
|
|
1297
|
-
- `FulfillmentStatuses`: `UNFULFILLED`, `PARTIALLY_FULFILLED`, `FULFILLED`
|
|
1298
|
-
- `AddressTypes`: `SHIPPING`, `BILLING`
|
|
1299
|
-
|
|
1300
|
-
## Error handling
|
|
1301
|
-
|
|
1302
|
-
```typescript
|
|
1303
|
-
import { BehioApiError, BehioNetworkError } from '@behio/storefront-sdk';
|
|
1304
|
-
|
|
1305
|
-
try {
|
|
1306
|
-
await shop.auth.login({ email: 'wrong', password: 'bad' });
|
|
1307
|
-
} catch (err) {
|
|
1308
|
-
if (err instanceof BehioApiError) {
|
|
1309
|
-
err.status; // HTTP status (401)
|
|
1310
|
-
err.code; // Typed error code ('INVALID_CREDENTIALS')
|
|
1311
|
-
err.message; // Human-readable message
|
|
1312
|
-
err.body; // Full API response
|
|
1313
|
-
err.isRetryable; // boolean
|
|
1314
|
-
err.is('INVALID_CREDENTIALS'); // typed helper
|
|
1315
|
-
}
|
|
1316
|
-
if (err instanceof BehioNetworkError) {
|
|
1317
|
-
err.code; // 'NETWORK_ERROR' | 'TIMEOUT'
|
|
1318
|
-
err.isRetryable; // always true
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
```
|
|
1322
|
-
|
|
1323
|
-
**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`
|
|
1324
|
-
|
|
1325
|
-
## Events & interceptors
|
|
1326
|
-
|
|
1327
|
-
```typescript
|
|
1328
|
-
// Events
|
|
1329
|
-
shop.on('auth:login', (data) => console.log('Logged in'));
|
|
1330
|
-
shop.on('auth:logout', () => console.log('Logged out'));
|
|
1331
|
-
shop.on('auth:token-refresh', () => console.log('Token refreshed'));
|
|
1332
|
-
shop.on('cart:updated', (cart) => console.log('Cart changed'));
|
|
1333
|
-
shop.on('order:created', (order) => console.log('Order placed'));
|
|
1334
|
-
shop.on('error', (err) => console.error('API error', err));
|
|
1335
|
-
shop.on('rate-limit-warning', ({ remaining }) => console.warn(`${remaining} left`));
|
|
1336
|
-
|
|
1337
|
-
// Unsubscribe
|
|
1338
|
-
const unsub = shop.on('error', handler);
|
|
1339
|
-
unsub();
|
|
1340
|
-
|
|
1341
|
-
// Request interceptor — modify outgoing requests
|
|
1342
|
-
shop.addRequestInterceptor((config) => {
|
|
1343
|
-
config.headers['X-Custom'] = 'value';
|
|
1344
|
-
return config;
|
|
1345
|
-
});
|
|
1346
|
-
|
|
1347
|
-
// Response interceptor — observe all responses
|
|
1348
|
-
shop.addResponseInterceptor((res) => {
|
|
1349
|
-
console.log(`${res.status}`);
|
|
1350
|
-
});
|
|
1351
|
-
```
|
|
1352
|
-
|
|
1353
|
-
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`
|
|
1354
|
-
|
|
1355
|
-
## Auto token refresh
|
|
1356
|
-
|
|
1357
|
-
When any authenticated request returns 401, the SDK automatically:
|
|
1358
|
-
1. Calls `auth.refresh()` with the stored refresh token
|
|
1359
|
-
2. Retries the original request with the new access token
|
|
1360
|
-
3. If refresh also fails → clears tokens, throws original error
|
|
1361
|
-
|
|
1362
|
-
No code needed. A concurrent-request lock prevents multiple simultaneous refreshes.
|
|
1363
|
-
|
|
1364
|
-
## Cart session management
|
|
1365
|
-
|
|
1366
|
-
Anonymous users get a `sessionToken` automatically on first `addItem`. It's stored in cookies/localStorage via `BehioProvider` (or manually via `shop.setCartSession()`).
|
|
1367
|
-
|
|
1368
|
-
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.
|
|
1369
|
-
|
|
1370
|
-
---
|
|
1371
|
-
|
|
1372
|
-
## React Query keys (for manual cache invalidation)
|
|
1373
|
-
|
|
1374
|
-
```typescript
|
|
1375
|
-
['behio', 'shop-info']
|
|
1376
|
-
['behio', 'products', queryString]
|
|
1377
|
-
['behio', 'product', slug]
|
|
1378
|
-
['behio', 'categories', locale?]
|
|
1379
|
-
['behio', 'labels', locale?]
|
|
1380
|
-
['behio', 'featured']
|
|
1381
|
-
['behio', 'filters']
|
|
1382
|
-
['behio', 'search', query]
|
|
1383
|
-
['behio', 'cart']
|
|
1384
|
-
['behio', 'customer']
|
|
1385
|
-
['behio', 'addresses']
|
|
1386
|
-
['behio', 'orders', params]
|
|
1387
|
-
['behio', 'order', orderNumber]
|
|
1388
|
-
['behio', 'pages', locale?]
|
|
1389
|
-
['behio', 'page', slug, locale?]
|
|
1390
|
-
```
|
|
117
|
+
**[sdk.behio.com](https://sdk.behio.com)**
|
|
1391
118
|
|
|
1392
119
|
## License
|
|
1393
120
|
|