@behio/storefront-sdk 0.38.0 → 0.42.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/dist/{chunk-FOUBL2EQ.js → chunk-3TPJG2WV.js} +568 -147
- package/dist/{chunk-MF5LMHZ4.mjs → chunk-QOZYQMK2.mjs} +566 -145
- package/dist/{client-Cb_eHKm9.d.mts → client-B0vlHxB1.d.mts} +312 -6
- package/dist/{client-Cb_eHKm9.d.ts → client-B0vlHxB1.d.ts} +312 -6
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +142 -40
- package/dist/index.mjs +111 -9
- package/dist/next.d.mts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.js +2 -2
- package/dist/next.mjs +1 -1
- package/dist/react.d.mts +3022 -4
- package/dist/react.d.ts +3022 -4
- package/dist/react.js +2584 -477
- package/dist/react.mjs +2090 -86
- package/package.json +1 -1
- package/dist/chunk-CZRSJULD.js +0 -118
- package/dist/chunk-QUU76QUB.mjs +0 -118
package/dist/react.mjs
CHANGED
|
@@ -1,19 +1,1702 @@
|
|
|
1
|
-
|
|
2
|
-
formatPrice,
|
|
3
|
-
generateVisitorId,
|
|
4
|
-
getStoredVisitorId,
|
|
5
|
-
grantAnalyticsConsent,
|
|
6
|
-
revokeAnalyticsConsent,
|
|
7
|
-
trackEcommerceEvent
|
|
8
|
-
} from "./chunk-QUU76QUB.mjs";
|
|
9
|
-
import {
|
|
10
|
-
BehioStorefront
|
|
11
|
-
} from "./chunk-MF5LMHZ4.mjs";
|
|
1
|
+
"use client";
|
|
12
2
|
|
|
13
3
|
// src/react/provider.tsx
|
|
14
4
|
import { useRef, useEffect, useMemo, useState, useCallback } from "react";
|
|
15
5
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
16
6
|
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var BehioApiError = class _BehioApiError extends Error {
|
|
9
|
+
constructor(status, body, message) {
|
|
10
|
+
super(message || `API Error ${status}`);
|
|
11
|
+
this.name = "BehioApiError";
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.body = body;
|
|
14
|
+
this.code = _BehioApiError.resolveCode(status, body);
|
|
15
|
+
this.isRetryable = status >= 500 || status === 429;
|
|
16
|
+
}
|
|
17
|
+
static resolveCode(status, body) {
|
|
18
|
+
if (status === 401) return "UNAUTHORIZED";
|
|
19
|
+
if (status === 403) return "FORBIDDEN";
|
|
20
|
+
if (status === 404) return "NOT_FOUND";
|
|
21
|
+
if (status === 409) return "EMAIL_ALREADY_EXISTS";
|
|
22
|
+
if (status === 429) return "RATE_LIMITED";
|
|
23
|
+
if (status >= 500) return "INTERNAL_ERROR";
|
|
24
|
+
const msg = (body?.message || "").toLowerCase();
|
|
25
|
+
if (msg.includes("invalid") && msg.includes("password"))
|
|
26
|
+
return "INVALID_CREDENTIALS";
|
|
27
|
+
if (msg.includes("invalid") && msg.includes("email"))
|
|
28
|
+
return "INVALID_CREDENTIALS";
|
|
29
|
+
if (msg.includes("cart") && msg.includes("empty")) return "CART_EMPTY";
|
|
30
|
+
if (msg.includes("product") && msg.includes("not found"))
|
|
31
|
+
return "PRODUCT_NOT_FOUND";
|
|
32
|
+
if (msg.includes("discount") && msg.includes("expired"))
|
|
33
|
+
return "DISCOUNT_EXPIRED";
|
|
34
|
+
if (msg.includes("discount") && msg.includes("invalid"))
|
|
35
|
+
return "INVALID_DISCOUNT";
|
|
36
|
+
if (msg.includes("token") && msg.includes("expired"))
|
|
37
|
+
return "TOKEN_EXPIRED";
|
|
38
|
+
if (msg.includes("cancel")) return "ORDER_NOT_CANCELLABLE";
|
|
39
|
+
if (status === 400) return "VALIDATION_ERROR";
|
|
40
|
+
return "UNKNOWN";
|
|
41
|
+
}
|
|
42
|
+
/** Check if this is a specific error type */
|
|
43
|
+
is(code) {
|
|
44
|
+
return this.code === code;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var BehioNetworkError = class extends Error {
|
|
48
|
+
constructor(message, isTimeout = false) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.isRetryable = true;
|
|
51
|
+
this.name = "BehioNetworkError";
|
|
52
|
+
this.code = isTimeout ? "TIMEOUT" : "NETWORK_ERROR";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function ok(data) {
|
|
56
|
+
return { data, error: null };
|
|
57
|
+
}
|
|
58
|
+
function toSdkError(err) {
|
|
59
|
+
if (err instanceof BehioApiError) {
|
|
60
|
+
return {
|
|
61
|
+
code: err.code,
|
|
62
|
+
message: err.message,
|
|
63
|
+
status: err.status,
|
|
64
|
+
body: err.body,
|
|
65
|
+
isRetryable: err.isRetryable,
|
|
66
|
+
cause: err
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (err instanceof BehioNetworkError) {
|
|
70
|
+
return {
|
|
71
|
+
code: err.code,
|
|
72
|
+
message: err.message,
|
|
73
|
+
status: null,
|
|
74
|
+
isRetryable: err.isRetryable,
|
|
75
|
+
cause: err
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
code: "UNKNOWN",
|
|
80
|
+
message: err instanceof Error ? err.message : String(err),
|
|
81
|
+
status: null,
|
|
82
|
+
isRetryable: false,
|
|
83
|
+
cause: err
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/client.ts
|
|
88
|
+
var BehioStorefront = class {
|
|
89
|
+
constructor(config) {
|
|
90
|
+
/** Consent-gated persistent visitor id — set by the analytics tracker. */
|
|
91
|
+
this.analyticsVisitorId = null;
|
|
92
|
+
// Token refresh lock
|
|
93
|
+
this.isRefreshing = false;
|
|
94
|
+
this.refreshPromise = null;
|
|
95
|
+
// Event emitter
|
|
96
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
97
|
+
// Interceptors
|
|
98
|
+
this.requestInterceptors = [];
|
|
99
|
+
this.responseInterceptors = [];
|
|
100
|
+
// Rate limit tracking
|
|
101
|
+
this.rateLimitRemaining = null;
|
|
102
|
+
this.rateLimitReset = null;
|
|
103
|
+
this.baseUrl = (config.baseUrl || "https://be.behio.com").replace(
|
|
104
|
+
/\/$/,
|
|
105
|
+
""
|
|
106
|
+
);
|
|
107
|
+
this.apiKey = config.apiKey;
|
|
108
|
+
this.shopDomain = config.shopDomain;
|
|
109
|
+
this.defaultLocale = config.locale;
|
|
110
|
+
this.defaultCurrency = config.currency;
|
|
111
|
+
this.fetchFn = config.fetch || globalThis.fetch.bind(globalThis);
|
|
112
|
+
this.timeout = config.timeout ?? 3e4;
|
|
113
|
+
this.retries = config.retries ?? 1;
|
|
114
|
+
this.retryDelay = config.retryDelay ?? 1e3;
|
|
115
|
+
this.catalog = new CatalogModule(this);
|
|
116
|
+
this.auth = new AuthModule(this);
|
|
117
|
+
this.cart = new CartModule(this);
|
|
118
|
+
this.checkout = new CheckoutModule(this);
|
|
119
|
+
this.orders = new OrdersModule(this);
|
|
120
|
+
this.customer = new CustomerModule(this);
|
|
121
|
+
this.pages = new PagesModule(this);
|
|
122
|
+
this.wishlist = new WishlistModule(this);
|
|
123
|
+
this.reviews = new ReviewsModule(this);
|
|
124
|
+
this.returns = new ReturnsModule(this);
|
|
125
|
+
this.consent = new ConsentModule(this);
|
|
126
|
+
this.quotes = new QuotesModule(this);
|
|
127
|
+
this.addresses = new AddressModule(this);
|
|
128
|
+
this.shipping = new ShippingModule(this);
|
|
129
|
+
this.newsletter = new NewsletterModule(this);
|
|
130
|
+
this.subscriptions = new SubscriptionsModule(this);
|
|
131
|
+
this.certificates = new CourseCertificatesModule(this);
|
|
132
|
+
}
|
|
133
|
+
// --- Public methods ---
|
|
134
|
+
/**
|
|
135
|
+
* Called by the analytics tracker when the visitor grants (id) or revokes
|
|
136
|
+
* (null) analytics consent. When set, requests carry the X-Behio-Vid header
|
|
137
|
+
* so the backend can attribute orders to the visitor journey.
|
|
138
|
+
*/
|
|
139
|
+
setAnalyticsVisitorId(id) {
|
|
140
|
+
this.analyticsVisitorId = id;
|
|
141
|
+
}
|
|
142
|
+
/** The consent-gated visitor id, if analytics consent was granted. */
|
|
143
|
+
getAnalyticsVisitorId() {
|
|
144
|
+
return this.analyticsVisitorId;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Fire-and-forget Behio Analytics ingest. Uses a bare keepalive fetch (not
|
|
148
|
+
* the interceptor pipeline) so flushes on pagehide still land, and swallows
|
|
149
|
+
* every error — analytics must never break the shop. The client sends no
|
|
150
|
+
* identity; the visitor hash is computed server-side from a daily salt.
|
|
151
|
+
*/
|
|
152
|
+
async sendAnalyticsEvents(input) {
|
|
153
|
+
try {
|
|
154
|
+
const headers = {
|
|
155
|
+
"X-Api-Key": this.apiKey,
|
|
156
|
+
"Content-Type": "application/json"
|
|
157
|
+
};
|
|
158
|
+
if (this.shopDomain) headers["X-Shop-Domain"] = this.shopDomain;
|
|
159
|
+
await fetch(`${this.baseUrl}/storefront/v1/analytics/events`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
keepalive: true,
|
|
162
|
+
headers,
|
|
163
|
+
body: JSON.stringify(input)
|
|
164
|
+
});
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Personal behavioral offers for this visitor (consent-gated id). Returns
|
|
170
|
+
* only the visitor's own offers; requires the analytics visitor id.
|
|
171
|
+
*/
|
|
172
|
+
async getPersonalOffers(visitorId) {
|
|
173
|
+
return this.request("GET", "/offers", { query: { visitorId } });
|
|
174
|
+
}
|
|
175
|
+
/** Email-gate completion: trade an e-mail for the personal discount code. */
|
|
176
|
+
async claimOfferByEmail(offerId, input) {
|
|
177
|
+
return this.request("POST", `/offers/${offerId}/claim-email`, {
|
|
178
|
+
body: input
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Get basic shop info */
|
|
182
|
+
async getShopInfo() {
|
|
183
|
+
return this.request("GET", "/shop");
|
|
184
|
+
}
|
|
185
|
+
/** Get SEO metadata for the shop homepage in the given locale (defaults to shop default). */
|
|
186
|
+
async getShopSeo(locale) {
|
|
187
|
+
return this.request("GET", "/shop/seo", {
|
|
188
|
+
query: locale ? { locale } : void 0
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get the merchant-defined scripts (analytics, pixels, verification, custom
|
|
193
|
+
* head/body markup) to inject into the storefront. Returns only enabled
|
|
194
|
+
* entries. Render typed entries into their snippet and RAW verbatim; gate
|
|
195
|
+
* any entry with `consentRequired` behind the visitor's analytics consent.
|
|
196
|
+
*/
|
|
197
|
+
async getShopScripts() {
|
|
198
|
+
return this.request("GET", "/shop/scripts");
|
|
199
|
+
}
|
|
200
|
+
/** Set auth tokens (e.g. from localStorage) */
|
|
201
|
+
setTokens(tokens) {
|
|
202
|
+
this.accessToken = tokens.accessToken;
|
|
203
|
+
this.refreshToken = tokens.refreshToken;
|
|
204
|
+
}
|
|
205
|
+
/** Clear auth tokens */
|
|
206
|
+
clearTokens() {
|
|
207
|
+
this.accessToken = void 0;
|
|
208
|
+
this.refreshToken = void 0;
|
|
209
|
+
}
|
|
210
|
+
/** Get current access token */
|
|
211
|
+
getAccessToken() {
|
|
212
|
+
return this.accessToken;
|
|
213
|
+
}
|
|
214
|
+
/** Get current refresh token */
|
|
215
|
+
getRefreshToken() {
|
|
216
|
+
return this.refreshToken;
|
|
217
|
+
}
|
|
218
|
+
/** Set cart session token (e.g. from cookie) */
|
|
219
|
+
setCartSession(token) {
|
|
220
|
+
this.cartSession = token;
|
|
221
|
+
}
|
|
222
|
+
/** Get cart session token */
|
|
223
|
+
getCartSession() {
|
|
224
|
+
return this.cartSession;
|
|
225
|
+
}
|
|
226
|
+
/** Clear cart session */
|
|
227
|
+
clearCartSession() {
|
|
228
|
+
this.cartSession = void 0;
|
|
229
|
+
}
|
|
230
|
+
// --- Default currency / locale (runtime-switchable) ---
|
|
231
|
+
/**
|
|
232
|
+
* Set the default currency sent on every catalog request (unless a per-call
|
|
233
|
+
* `currency` overrides it). Read live at request time, so changing it takes
|
|
234
|
+
* effect immediately without rebuilding the client. Pass undefined to clear
|
|
235
|
+
* (falls back to the shop's default currency server-side).
|
|
236
|
+
*/
|
|
237
|
+
setCurrency(currency) {
|
|
238
|
+
this.defaultCurrency = currency || void 0;
|
|
239
|
+
}
|
|
240
|
+
/** Get the current default currency, if any. */
|
|
241
|
+
getCurrency() {
|
|
242
|
+
return this.defaultCurrency;
|
|
243
|
+
}
|
|
244
|
+
/** Set the default locale sent on every catalog request (per-call wins). */
|
|
245
|
+
setLocale(locale) {
|
|
246
|
+
this.defaultLocale = locale || void 0;
|
|
247
|
+
}
|
|
248
|
+
/** Get the current default locale, if any. */
|
|
249
|
+
getLocale() {
|
|
250
|
+
return this.defaultLocale;
|
|
251
|
+
}
|
|
252
|
+
// --- Event emitter ---
|
|
253
|
+
/** Subscribe to SDK events. Returns an unsubscribe function. */
|
|
254
|
+
on(event, handler) {
|
|
255
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
256
|
+
this.listeners.get(event).add(handler);
|
|
257
|
+
return () => {
|
|
258
|
+
this.listeners.get(event)?.delete(handler);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** @internal Emit an event (fire-and-forget, handler errors are swallowed) */
|
|
262
|
+
emit(event, data) {
|
|
263
|
+
this.listeners.get(event)?.forEach((fn) => {
|
|
264
|
+
try {
|
|
265
|
+
fn(data);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// --- Interceptors ---
|
|
271
|
+
/** Add a request interceptor. Returns an unsubscribe function. */
|
|
272
|
+
addRequestInterceptor(fn) {
|
|
273
|
+
this.requestInterceptors.push(fn);
|
|
274
|
+
return () => {
|
|
275
|
+
this.requestInterceptors = this.requestInterceptors.filter(
|
|
276
|
+
(f) => f !== fn
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** Add a response interceptor. Returns an unsubscribe function. */
|
|
281
|
+
addResponseInterceptor(fn) {
|
|
282
|
+
this.responseInterceptors.push(fn);
|
|
283
|
+
return () => {
|
|
284
|
+
this.responseInterceptors = this.responseInterceptors.filter(
|
|
285
|
+
(f) => f !== fn
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// --- Rate limit ---
|
|
290
|
+
/** Get current rate limit info from latest response headers */
|
|
291
|
+
getRateLimitInfo() {
|
|
292
|
+
return { remaining: this.rateLimitRemaining, reset: this.rateLimitReset };
|
|
293
|
+
}
|
|
294
|
+
// --- Token refresh ---
|
|
295
|
+
async handleTokenRefresh() {
|
|
296
|
+
if (this.isRefreshing) {
|
|
297
|
+
if (this.refreshPromise) await this.refreshPromise;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.isRefreshing = true;
|
|
301
|
+
this.refreshPromise = (async () => {
|
|
302
|
+
try {
|
|
303
|
+
const res = await this.auth.refresh();
|
|
304
|
+
if (res.error) {
|
|
305
|
+
this.clearTokens();
|
|
306
|
+
this.emit("auth:token-refresh-failed");
|
|
307
|
+
throw new BehioApiError(401, null, "Token refresh failed");
|
|
308
|
+
}
|
|
309
|
+
this.emit("auth:token-refresh");
|
|
310
|
+
} catch (err) {
|
|
311
|
+
this.clearTokens();
|
|
312
|
+
this.emit("auth:token-refresh-failed");
|
|
313
|
+
if (err instanceof BehioApiError) throw err;
|
|
314
|
+
throw new BehioApiError(401, null, "Token refresh failed");
|
|
315
|
+
} finally {
|
|
316
|
+
this.isRefreshing = false;
|
|
317
|
+
this.refreshPromise = null;
|
|
318
|
+
}
|
|
319
|
+
})();
|
|
320
|
+
return this.refreshPromise;
|
|
321
|
+
}
|
|
322
|
+
// --- Public request wrapper (SdkResult) ---
|
|
323
|
+
/**
|
|
324
|
+
* Every public module method funnels through here. Internally calls
|
|
325
|
+
* `rawRequest` (which throws on failure) and maps thrown errors to
|
|
326
|
+
* `SdkError` so the public surface can return `SdkResult<T>`.
|
|
327
|
+
*
|
|
328
|
+
* @internal — don't call from outside the SDK; use the typed module
|
|
329
|
+
* methods (behio.catalog.*, behio.cart.*, …) instead.
|
|
330
|
+
*/
|
|
331
|
+
async request(method, path, options) {
|
|
332
|
+
try {
|
|
333
|
+
const data = await this.rawRequest(method, path, options);
|
|
334
|
+
return ok(data);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return { data: null, error: toSdkError(err) };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Binary GET (PDF downloads). Same auth headers as `request()`, but the
|
|
341
|
+
* response is returned as a Blob instead of parsed JSON.
|
|
342
|
+
*
|
|
343
|
+
* @internal — use the typed module methods (behio.certificates.downloadPdf).
|
|
344
|
+
*/
|
|
345
|
+
async requestBlob(path) {
|
|
346
|
+
try {
|
|
347
|
+
const headers = { "X-Api-Key": this.apiKey };
|
|
348
|
+
if (this.accessToken)
|
|
349
|
+
headers.Authorization = `Bearer ${this.accessToken}`;
|
|
350
|
+
const res = await this.fetchFn(`${this.baseUrl}/storefront/v1${path}`, {
|
|
351
|
+
headers
|
|
352
|
+
});
|
|
353
|
+
if (!res.ok) {
|
|
354
|
+
let body = null;
|
|
355
|
+
try {
|
|
356
|
+
body = await res.json();
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
throw new BehioApiError(res.status, body);
|
|
360
|
+
}
|
|
361
|
+
return ok(await res.blob());
|
|
362
|
+
} catch (err) {
|
|
363
|
+
return { data: null, error: toSdkError(err) };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Throws on failure (API error / network / timeout). Kept private so
|
|
368
|
+
* internal auth refresh recursion keeps its existing control flow —
|
|
369
|
+
* public callers must go through `request()` which returns Result.
|
|
370
|
+
*/
|
|
371
|
+
async rawRequest(method, path, options) {
|
|
372
|
+
const params = new URLSearchParams();
|
|
373
|
+
if (options?.query) {
|
|
374
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
375
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
376
|
+
if (Array.isArray(value)) {
|
|
377
|
+
for (const v of value) {
|
|
378
|
+
if (v !== void 0 && v !== null && v !== "") {
|
|
379
|
+
params.append(key, String(v));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
params.set(key, String(value));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (this.defaultLocale && !params.has("locale")) {
|
|
388
|
+
params.set("locale", this.defaultLocale);
|
|
389
|
+
}
|
|
390
|
+
if (this.defaultCurrency && !params.has("currency")) {
|
|
391
|
+
params.set("currency", this.defaultCurrency);
|
|
392
|
+
}
|
|
393
|
+
const qs = params.toString();
|
|
394
|
+
const url = `${this.baseUrl}/storefront/v1${path}${qs ? `?${qs}` : ""}`;
|
|
395
|
+
const headers = {
|
|
396
|
+
"X-Api-Key": this.apiKey,
|
|
397
|
+
"Content-Type": "application/json"
|
|
398
|
+
};
|
|
399
|
+
if (this.shopDomain) {
|
|
400
|
+
headers["X-Shop-Domain"] = this.shopDomain;
|
|
401
|
+
}
|
|
402
|
+
if (this.accessToken && options?.auth !== false) {
|
|
403
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
404
|
+
}
|
|
405
|
+
if (this.cartSession) {
|
|
406
|
+
headers["X-Cart-Session"] = this.cartSession;
|
|
407
|
+
}
|
|
408
|
+
if (this.analyticsVisitorId) {
|
|
409
|
+
headers["X-Behio-Vid"] = this.analyticsVisitorId;
|
|
410
|
+
}
|
|
411
|
+
if (options?.headers) {
|
|
412
|
+
Object.assign(headers, options.headers);
|
|
413
|
+
}
|
|
414
|
+
const bodyStr = options?.body ? JSON.stringify(options.body) : void 0;
|
|
415
|
+
let interceptedConfig = {
|
|
416
|
+
url,
|
|
417
|
+
method,
|
|
418
|
+
headers,
|
|
419
|
+
body: bodyStr
|
|
420
|
+
};
|
|
421
|
+
for (const interceptor of this.requestInterceptors) {
|
|
422
|
+
interceptedConfig = await interceptor(interceptedConfig);
|
|
423
|
+
}
|
|
424
|
+
this.emit("request", { method, path });
|
|
425
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
426
|
+
const controller = new AbortController();
|
|
427
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
428
|
+
if (options?.signal) {
|
|
429
|
+
if (options.signal.aborted) {
|
|
430
|
+
clearTimeout(timeoutId);
|
|
431
|
+
throw new BehioNetworkError("Request aborted", false);
|
|
432
|
+
}
|
|
433
|
+
options.signal.addEventListener("abort", () => controller.abort(), {
|
|
434
|
+
once: true
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
let res;
|
|
438
|
+
try {
|
|
439
|
+
res = await this.fetchFn(interceptedConfig.url, {
|
|
440
|
+
method: interceptedConfig.method,
|
|
441
|
+
headers: interceptedConfig.headers,
|
|
442
|
+
body: interceptedConfig.body,
|
|
443
|
+
signal: controller.signal
|
|
444
|
+
});
|
|
445
|
+
} catch (err) {
|
|
446
|
+
clearTimeout(timeoutId);
|
|
447
|
+
const isAbort = err instanceof DOMException && err.name === "AbortError";
|
|
448
|
+
const networkErr = new BehioNetworkError(
|
|
449
|
+
isAbort ? "Request timed out" : err.message || "Network error",
|
|
450
|
+
isAbort
|
|
451
|
+
);
|
|
452
|
+
this.emit("error", networkErr);
|
|
453
|
+
if (attempt < this.retries) {
|
|
454
|
+
await new Promise(
|
|
455
|
+
(r) => setTimeout(r, this.retryDelay * (attempt + 1))
|
|
456
|
+
);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
throw networkErr;
|
|
460
|
+
} finally {
|
|
461
|
+
clearTimeout(timeoutId);
|
|
462
|
+
}
|
|
463
|
+
const remaining = res.headers.get("X-RateLimit-Remaining");
|
|
464
|
+
const reset = res.headers.get("X-RateLimit-Reset");
|
|
465
|
+
if (remaining) this.rateLimitRemaining = parseInt(remaining, 10);
|
|
466
|
+
if (reset) this.rateLimitReset = parseInt(reset, 10);
|
|
467
|
+
if (this.rateLimitRemaining !== null && this.rateLimitRemaining <= 5) {
|
|
468
|
+
this.emit("rate-limit-warning", {
|
|
469
|
+
remaining: this.rateLimitRemaining,
|
|
470
|
+
reset: this.rateLimitReset
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
this.emit("response", { method, path, status: res.status });
|
|
474
|
+
if (!res.ok) {
|
|
475
|
+
const body = await res.json().catch(() => null);
|
|
476
|
+
const apiError = new BehioApiError(
|
|
477
|
+
res.status,
|
|
478
|
+
body,
|
|
479
|
+
body?.message || `API Error ${res.status}`
|
|
480
|
+
);
|
|
481
|
+
if (res.status === 401 && this.refreshToken && options?.auth !== false && !options?._isRetryAfterRefresh) {
|
|
482
|
+
try {
|
|
483
|
+
await this.handleTokenRefresh();
|
|
484
|
+
return this.rawRequest(method, path, {
|
|
485
|
+
...options,
|
|
486
|
+
_isRetryAfterRefresh: true
|
|
487
|
+
});
|
|
488
|
+
} catch {
|
|
489
|
+
this.emit("error", apiError);
|
|
490
|
+
throw apiError;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
this.emit("error", apiError);
|
|
494
|
+
if (attempt < this.retries && apiError.isRetryable) {
|
|
495
|
+
await new Promise(
|
|
496
|
+
(r) => setTimeout(r, this.retryDelay * (attempt + 1))
|
|
497
|
+
);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
throw apiError;
|
|
501
|
+
}
|
|
502
|
+
const responseData = res.status === 204 ? null : await res.json();
|
|
503
|
+
for (const interceptor of this.responseInterceptors) {
|
|
504
|
+
try {
|
|
505
|
+
await interceptor({
|
|
506
|
+
status: res.status,
|
|
507
|
+
data: responseData,
|
|
508
|
+
headers: res.headers
|
|
509
|
+
});
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return responseData;
|
|
514
|
+
}
|
|
515
|
+
throw new BehioNetworkError("Request failed after retries");
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
var CatalogModule = class {
|
|
519
|
+
constructor(client) {
|
|
520
|
+
this.client = client;
|
|
521
|
+
}
|
|
522
|
+
/** List products with filtering, pagination, search */
|
|
523
|
+
async getProducts(query) {
|
|
524
|
+
const q = {};
|
|
525
|
+
if (query) {
|
|
526
|
+
if (query.page) q.page = query.page;
|
|
527
|
+
if (query.limit) q.limit = query.limit;
|
|
528
|
+
if (query.category) q.category = query.category;
|
|
529
|
+
if (query.label) q.label = query.label;
|
|
530
|
+
if (query.priceMin) q.priceMin = query.priceMin;
|
|
531
|
+
if (query.priceMax) q.priceMax = query.priceMax;
|
|
532
|
+
if (query.currency) q.currency = query.currency;
|
|
533
|
+
if (query.locale) q.locale = query.locale;
|
|
534
|
+
if (query.sort) q.sort = query.sort;
|
|
535
|
+
if (query.inStock !== void 0) q.inStock = query.inStock;
|
|
536
|
+
if (query.ratingMin !== void 0) q.ratingMin = query.ratingMin;
|
|
537
|
+
if (query.search) q.search = query.search;
|
|
538
|
+
if (query.customFields)
|
|
539
|
+
q.customFields = JSON.stringify(query.customFields);
|
|
540
|
+
if (query.facets) q.facets = JSON.stringify(query.facets);
|
|
541
|
+
if (query.ids && query.ids.length > 0) q.ids = query.ids;
|
|
542
|
+
if (query.slugs && query.slugs.length > 0) q.slugs = query.slugs;
|
|
543
|
+
if (query.labels && query.labels.length > 0) q.labels = query.labels;
|
|
544
|
+
if (query.categories && query.categories.length > 0)
|
|
545
|
+
q.categories = query.categories;
|
|
546
|
+
if (query.excludeIds && query.excludeIds.length > 0)
|
|
547
|
+
q.excludeIds = query.excludeIds;
|
|
548
|
+
if (query.excludeCategories && query.excludeCategories.length > 0)
|
|
549
|
+
q.excludeCategories = query.excludeCategories;
|
|
550
|
+
if (query.hasDiscount !== void 0) q.hasDiscount = query.hasDiscount;
|
|
551
|
+
if (query.isFeatured !== void 0) q.isFeatured = query.isFeatured;
|
|
552
|
+
if (query.createdAfter !== void 0) q.createdAfter = query.createdAfter;
|
|
553
|
+
}
|
|
554
|
+
return this.client.request(
|
|
555
|
+
"GET",
|
|
556
|
+
"/catalog/products",
|
|
557
|
+
{ query: q }
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
/** Get product detail by slug */
|
|
561
|
+
async getProduct(slug, options) {
|
|
562
|
+
return this.client.request(
|
|
563
|
+
"GET",
|
|
564
|
+
`/catalog/products/${slug}`,
|
|
565
|
+
{
|
|
566
|
+
query: { locale: options?.locale, currency: options?.currency }
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
/** Get category tree */
|
|
571
|
+
async getCategories(locale) {
|
|
572
|
+
const res = await this.client.request("GET", "/catalog/categories", { query: { locale } });
|
|
573
|
+
if (res.error) return res;
|
|
574
|
+
return ok({ categories: res.data.categories || res.data.items || [] });
|
|
575
|
+
}
|
|
576
|
+
/** Get category detail by slug */
|
|
577
|
+
async getCategory(slug, locale) {
|
|
578
|
+
return this.client.request(
|
|
579
|
+
"GET",
|
|
580
|
+
`/catalog/categories/${slug}`,
|
|
581
|
+
{ query: { locale } }
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
/** Get products in a category */
|
|
585
|
+
async getCategoryProducts(slug, query) {
|
|
586
|
+
const q = {};
|
|
587
|
+
if (query) {
|
|
588
|
+
if (query.page) q.page = query.page;
|
|
589
|
+
if (query.limit) q.limit = query.limit;
|
|
590
|
+
if (query.sort) q.sort = query.sort;
|
|
591
|
+
if (query.locale) q.locale = query.locale;
|
|
592
|
+
if (query.currency) q.currency = query.currency;
|
|
593
|
+
}
|
|
594
|
+
return this.client.request(
|
|
595
|
+
"GET",
|
|
596
|
+
`/catalog/categories/${slug}/products`,
|
|
597
|
+
{ query: q }
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Resolved navigation menu by handle (e.g. "main", "footer"), built in the
|
|
602
|
+
* admin Navigace. Item labels resolve in `locale`; typed refs come back as
|
|
603
|
+
* {type, slug} for link building. Returns an SdkResult error (404) when the
|
|
604
|
+
* handle is unknown or inactive — callers fall back to their own source.
|
|
605
|
+
*/
|
|
606
|
+
async getMenu(handle, options) {
|
|
607
|
+
return this.client.request(
|
|
608
|
+
"GET",
|
|
609
|
+
`/catalog/menu/${encodeURIComponent(handle)}`,
|
|
610
|
+
{
|
|
611
|
+
query: { locale: options?.locale }
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
/** Get all labels */
|
|
616
|
+
async getLabels(locale) {
|
|
617
|
+
const res = await this.client.request("GET", "/catalog/labels", { query: { locale } });
|
|
618
|
+
if (res.error) return res;
|
|
619
|
+
return ok({ labels: res.data.labels || res.data.items || [] });
|
|
620
|
+
}
|
|
621
|
+
/** Get featured products */
|
|
622
|
+
async getFeatured(options) {
|
|
623
|
+
return this.client.request(
|
|
624
|
+
"GET",
|
|
625
|
+
"/catalog/featured",
|
|
626
|
+
{
|
|
627
|
+
query: { locale: options?.locale, currency: options?.currency }
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
/** Get available filter fields for dynamic filter UI */
|
|
632
|
+
async getFilters() {
|
|
633
|
+
return this.client.request(
|
|
634
|
+
"GET",
|
|
635
|
+
"/catalog/filters"
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Facet groups + selection-aware counts for the current filter set (custom
|
|
640
|
+
* fields, labels, price, availability, rating, subcategories). Pass the SAME
|
|
641
|
+
* query you pass to `getProducts` (category, search, price, inStock, ratingMin,
|
|
642
|
+
* customFields, facets slugs, labels): counts for each facet are computed with
|
|
643
|
+
* that facet excluded, and values that drop to 0 are still returned (render
|
|
644
|
+
* them disabled). Use this to build an Alza-style filter sidebar.
|
|
645
|
+
*/
|
|
646
|
+
async getFacets(query) {
|
|
647
|
+
const q = {};
|
|
648
|
+
if (query) {
|
|
649
|
+
if (query.category) q.category = query.category;
|
|
650
|
+
if (query.label) q.label = query.label;
|
|
651
|
+
if (query.priceMin) q.priceMin = query.priceMin;
|
|
652
|
+
if (query.priceMax) q.priceMax = query.priceMax;
|
|
653
|
+
if (query.currency) q.currency = query.currency;
|
|
654
|
+
if (query.locale) q.locale = query.locale;
|
|
655
|
+
if (query.inStock !== void 0) q.inStock = query.inStock;
|
|
656
|
+
if (query.ratingMin !== void 0) q.ratingMin = query.ratingMin;
|
|
657
|
+
if (query.search) q.search = query.search;
|
|
658
|
+
if (query.customFields)
|
|
659
|
+
q.customFields = JSON.stringify(query.customFields);
|
|
660
|
+
if (query.facets) q.facets = JSON.stringify(query.facets);
|
|
661
|
+
if (query.labels && query.labels.length > 0) q.labels = query.labels;
|
|
662
|
+
if (query.categories && query.categories.length > 0)
|
|
663
|
+
q.categories = query.categories;
|
|
664
|
+
if (query.excludeCategories && query.excludeCategories.length > 0)
|
|
665
|
+
q.excludeCategories = query.excludeCategories;
|
|
666
|
+
}
|
|
667
|
+
return this.client.request("GET", "/catalog/facets", {
|
|
668
|
+
query: q
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/** Search products */
|
|
672
|
+
async search(query, options) {
|
|
673
|
+
return this.getProducts({ search: query, ...options });
|
|
674
|
+
}
|
|
675
|
+
/** List all active bundles */
|
|
676
|
+
async getBundles() {
|
|
677
|
+
return this.client.request("GET", "/catalog/bundles");
|
|
678
|
+
}
|
|
679
|
+
/** Get a single bundle by slug */
|
|
680
|
+
async getBundle(slug) {
|
|
681
|
+
return this.client.request("GET", `/catalog/bundles/${slug}`);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* One product group ("collection") by slug with its products as standard
|
|
685
|
+
* list items. Groups are curated in the admin; use this to render curated
|
|
686
|
+
* product bands. Returns an SdkResult error (404) when the group is
|
|
687
|
+
* missing or inactive — callers should render nothing in that case.
|
|
688
|
+
*/
|
|
689
|
+
async getProductGroup(slug, options) {
|
|
690
|
+
return this.client.request(
|
|
691
|
+
"GET",
|
|
692
|
+
`/catalog/product-groups/${encodeURIComponent(slug)}`,
|
|
693
|
+
{
|
|
694
|
+
query: { locale: options?.locale, currency: options?.currency }
|
|
695
|
+
}
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Cross-sell / related / upsell products for a product. Returns three
|
|
700
|
+
* separate lists: `related` (podobné produkty), `upsell` (dražší
|
|
701
|
+
* alternativy) and `crossSell` (doporučené k nákupu). Items are localized
|
|
702
|
+
* and priced in the requested currency, ready to render with the same card
|
|
703
|
+
* component as `getFeatured` / `getProductGroup`.
|
|
704
|
+
*/
|
|
705
|
+
async getCrossSell(productSlug, options) {
|
|
706
|
+
return this.client.request(
|
|
707
|
+
"GET",
|
|
708
|
+
`/catalog/products/${encodeURIComponent(productSlug)}/cross-sell`,
|
|
709
|
+
{ query: { locale: options?.locale, currency: options?.currency } }
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
/** Active promotions applicable to a product (with countdown end time) */
|
|
713
|
+
/** Back-in-stock notification subscription for a sold-out product. */
|
|
714
|
+
async notifyWhenAvailable(productId, email) {
|
|
715
|
+
return this.client.request(
|
|
716
|
+
"POST",
|
|
717
|
+
`/catalog/products/${productId}/notify-when-available`,
|
|
718
|
+
{ body: { email } }
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
async getProductPromotions(productSlug) {
|
|
722
|
+
return this.client.request(
|
|
723
|
+
"GET",
|
|
724
|
+
`/catalog/products/${productSlug}/promotions`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
/** Check a gift card code — returns validity and remaining balance */
|
|
728
|
+
async checkGiftCard(code) {
|
|
729
|
+
return this.client.request(
|
|
730
|
+
"GET",
|
|
731
|
+
`/catalog/gift-cards/${encodeURIComponent(code)}/check`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Buy a gift card (GAP-39): creates a cart-independent order for the chosen
|
|
736
|
+
* amount. Redirect the customer to `paymentRedirectUrl` when present; the
|
|
737
|
+
* code is generated and emailed to the recipient once the order is paid.
|
|
738
|
+
*/
|
|
739
|
+
async purchaseGiftCard(input) {
|
|
740
|
+
return this.client.request(
|
|
741
|
+
"POST",
|
|
742
|
+
"/gift-cards/purchase",
|
|
743
|
+
{ body: input }
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
/** List configured payment methods (filtered by currency). */
|
|
747
|
+
async listPaymentMethods(opts) {
|
|
748
|
+
const query = {};
|
|
749
|
+
if (opts?.currency) query.currency = opts.currency;
|
|
750
|
+
return this.client.request(
|
|
751
|
+
"GET",
|
|
752
|
+
"/catalog/payment-methods",
|
|
753
|
+
{ query }
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
var AuthModule = class {
|
|
758
|
+
constructor(client) {
|
|
759
|
+
this.client = client;
|
|
760
|
+
}
|
|
761
|
+
/** Register a new customer */
|
|
762
|
+
async register(input) {
|
|
763
|
+
const res = await this.client.request(
|
|
764
|
+
"POST",
|
|
765
|
+
"/auth/register",
|
|
766
|
+
{
|
|
767
|
+
body: input,
|
|
768
|
+
auth: false
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
if (res.error) return res;
|
|
772
|
+
if (res.data.accessToken && res.data.refreshToken) {
|
|
773
|
+
this.client.setTokens({
|
|
774
|
+
accessToken: res.data.accessToken,
|
|
775
|
+
refreshToken: res.data.refreshToken
|
|
776
|
+
});
|
|
777
|
+
this.client.emit("auth:login", { email: input.email });
|
|
778
|
+
}
|
|
779
|
+
return res;
|
|
780
|
+
}
|
|
781
|
+
/** Login with email and password */
|
|
782
|
+
async login(input) {
|
|
783
|
+
const res = await this.client.request("POST", "/auth/login", {
|
|
784
|
+
body: input,
|
|
785
|
+
auth: false
|
|
786
|
+
});
|
|
787
|
+
if (res.error) return res;
|
|
788
|
+
this.client.setTokens(res.data);
|
|
789
|
+
this.client.emit("auth:login", { email: input.email });
|
|
790
|
+
return res;
|
|
791
|
+
}
|
|
792
|
+
/** Refresh access token using refresh token */
|
|
793
|
+
async refresh(refreshToken) {
|
|
794
|
+
const token = refreshToken || this.client.getRefreshToken();
|
|
795
|
+
if (!token) {
|
|
796
|
+
return {
|
|
797
|
+
data: null,
|
|
798
|
+
error: {
|
|
799
|
+
code: "UNAUTHORIZED",
|
|
800
|
+
message: "No refresh token available",
|
|
801
|
+
status: null,
|
|
802
|
+
isRetryable: false
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const res = await this.client.request("POST", "/auth/refresh", {
|
|
807
|
+
body: { refreshToken: token },
|
|
808
|
+
auth: false
|
|
809
|
+
});
|
|
810
|
+
if (res.error) return res;
|
|
811
|
+
this.client.setTokens(res.data);
|
|
812
|
+
return res;
|
|
813
|
+
}
|
|
814
|
+
/** Logout (invalidate refresh token) */
|
|
815
|
+
async logout(refreshToken) {
|
|
816
|
+
const token = refreshToken || this.client.getRefreshToken();
|
|
817
|
+
const res = await this.client.request(
|
|
818
|
+
"POST",
|
|
819
|
+
"/auth/logout",
|
|
820
|
+
{
|
|
821
|
+
body: { refreshToken: token }
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
this.client.clearTokens();
|
|
825
|
+
this.client.emit("auth:logout");
|
|
826
|
+
return res;
|
|
827
|
+
}
|
|
828
|
+
/** Request password reset email */
|
|
829
|
+
async forgotPassword(email) {
|
|
830
|
+
return this.client.request(
|
|
831
|
+
"POST",
|
|
832
|
+
"/auth/forgot-password",
|
|
833
|
+
{
|
|
834
|
+
body: { email },
|
|
835
|
+
auth: false
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
/** Reset password with token */
|
|
840
|
+
async resetPassword(token, newPassword) {
|
|
841
|
+
return this.client.request(
|
|
842
|
+
"POST",
|
|
843
|
+
"/auth/reset-password",
|
|
844
|
+
{
|
|
845
|
+
body: { token, newPassword },
|
|
846
|
+
auth: false
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
/** Verify email with token */
|
|
851
|
+
async verifyEmail(token) {
|
|
852
|
+
return this.client.request("POST", "/auth/verify-email", {
|
|
853
|
+
body: { token },
|
|
854
|
+
auth: false
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
/** Check if user is logged in (has access token) */
|
|
858
|
+
isLoggedIn() {
|
|
859
|
+
return !!this.client.getAccessToken();
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
var CartModule = class {
|
|
863
|
+
constructor(client) {
|
|
864
|
+
this.client = client;
|
|
865
|
+
}
|
|
866
|
+
/** Get current cart */
|
|
867
|
+
async get() {
|
|
868
|
+
return this.client.request("GET", "/cart");
|
|
869
|
+
}
|
|
870
|
+
/** Add item to cart */
|
|
871
|
+
async addItem(input) {
|
|
872
|
+
const res = await this.client.request(
|
|
873
|
+
"POST",
|
|
874
|
+
"/cart/items",
|
|
875
|
+
{
|
|
876
|
+
body: { productId: input.productId, quantity: input.quantity }
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
if (res.error) return res;
|
|
880
|
+
if (res.data.newSessionToken) {
|
|
881
|
+
this.client.setCartSession(res.data.newSessionToken);
|
|
882
|
+
}
|
|
883
|
+
this.client.emit("cart:updated", res.data);
|
|
884
|
+
return res;
|
|
885
|
+
}
|
|
886
|
+
/** Update item quantity */
|
|
887
|
+
async updateQuantity(itemId, quantity) {
|
|
888
|
+
const res = await this.client.request(
|
|
889
|
+
"PATCH",
|
|
890
|
+
`/cart/items/${itemId}`,
|
|
891
|
+
{
|
|
892
|
+
body: { quantity }
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
if (res.error) return res;
|
|
896
|
+
this.client.emit("cart:updated", res.data);
|
|
897
|
+
return res;
|
|
898
|
+
}
|
|
899
|
+
/** Remove item from cart */
|
|
900
|
+
async removeItem(itemId) {
|
|
901
|
+
const res = await this.client.request(
|
|
902
|
+
"DELETE",
|
|
903
|
+
`/cart/items/${itemId}`
|
|
904
|
+
);
|
|
905
|
+
if (res.error) return res;
|
|
906
|
+
this.client.emit("cart:updated", res.data);
|
|
907
|
+
return res;
|
|
908
|
+
}
|
|
909
|
+
/** Clear entire cart */
|
|
910
|
+
async clear() {
|
|
911
|
+
const res = await this.client.request("DELETE", "/cart");
|
|
912
|
+
if (res.error) return res;
|
|
913
|
+
this.client.emit("cart:cleared");
|
|
914
|
+
return res;
|
|
915
|
+
}
|
|
916
|
+
/** Apply a gift card code to the cart. Balance is deducted at checkout. */
|
|
917
|
+
async applyGiftCard(code) {
|
|
918
|
+
const res = await this.client.request("POST", "/cart/gift-card", {
|
|
919
|
+
body: { code }
|
|
920
|
+
});
|
|
921
|
+
if (res.error) return res;
|
|
922
|
+
this.client.emit("cart:updated", res.data);
|
|
923
|
+
return res;
|
|
924
|
+
}
|
|
925
|
+
/** Remove a specific applied gift card from the cart by its code. */
|
|
926
|
+
async removeGiftCard(code) {
|
|
927
|
+
const res = await this.client.request(
|
|
928
|
+
"DELETE",
|
|
929
|
+
`/cart/gift-card/${encodeURIComponent(code)}`
|
|
930
|
+
);
|
|
931
|
+
if (res.error) return res;
|
|
932
|
+
this.client.emit("cart:updated", res.data);
|
|
933
|
+
return res;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Add a bundle to the cart. Price is snapshotted at the bundle's current
|
|
937
|
+
* price. Pass either the bundle id or its slug — slug is more ergonomic
|
|
938
|
+
* for static storefront wiring (`behio.cart.addBundle({slug: "morning-set"})`).
|
|
939
|
+
*
|
|
940
|
+
* Respects the bundle's `minQuantity`, `maxQuantity`, and `stockLimit`:
|
|
941
|
+
* the request rejects with HTTP 400 if the resulting cart line would
|
|
942
|
+
* violate any of them. The returned error includes the relevant field
|
|
943
|
+
* (`minQuantity`, `maxQuantity`, or `remaining`) so the storefront can
|
|
944
|
+
* surface a meaningful message.
|
|
945
|
+
*
|
|
946
|
+
* @param identifier Either `{id: bundleId}` or `{slug: bundleSlug}`. As a
|
|
947
|
+
* convenience, passing a plain string is treated as the
|
|
948
|
+
* bundle id for backwards compatibility.
|
|
949
|
+
* @param quantity How many bundles to add (defaults to 1). Capped by
|
|
950
|
+
* the bundle's `maxQuantity` if set.
|
|
951
|
+
*/
|
|
952
|
+
async addBundle(identifier, quantity = 1) {
|
|
953
|
+
const body = {
|
|
954
|
+
quantity
|
|
955
|
+
};
|
|
956
|
+
if (typeof identifier === "string") {
|
|
957
|
+
body.bundleId = identifier;
|
|
958
|
+
} else if ("id" in identifier) {
|
|
959
|
+
body.bundleId = identifier.id;
|
|
960
|
+
} else {
|
|
961
|
+
body.bundleSlug = identifier.slug;
|
|
962
|
+
}
|
|
963
|
+
const res = await this.client.request("POST", "/cart/bundles", {
|
|
964
|
+
body
|
|
965
|
+
});
|
|
966
|
+
if (res.error) return res;
|
|
967
|
+
this.client.emit("cart:updated", res.data);
|
|
968
|
+
return res;
|
|
969
|
+
}
|
|
970
|
+
/** Update quantity of a bundle already in the cart */
|
|
971
|
+
async updateBundleQuantity(bundleId, quantity) {
|
|
972
|
+
const res = await this.client.request(
|
|
973
|
+
"PATCH",
|
|
974
|
+
`/cart/bundles/${bundleId}`,
|
|
975
|
+
{
|
|
976
|
+
body: { quantity }
|
|
977
|
+
}
|
|
978
|
+
);
|
|
979
|
+
if (res.error) return res;
|
|
980
|
+
this.client.emit("cart:updated", res.data);
|
|
981
|
+
return res;
|
|
982
|
+
}
|
|
983
|
+
/** Remove a bundle from the cart */
|
|
984
|
+
async removeBundle(bundleId) {
|
|
985
|
+
const res = await this.client.request(
|
|
986
|
+
"DELETE",
|
|
987
|
+
`/cart/bundles/${bundleId}`
|
|
988
|
+
);
|
|
989
|
+
if (res.error) return res;
|
|
990
|
+
this.client.emit("cart:updated", res.data);
|
|
991
|
+
return res;
|
|
992
|
+
}
|
|
993
|
+
/** Merge anonymous cart into authenticated customer cart */
|
|
994
|
+
async merge() {
|
|
995
|
+
const res = await this.client.request("POST", "/cart/merge");
|
|
996
|
+
if (res.error) return res;
|
|
997
|
+
this.client.emit("cart:updated", res.data);
|
|
998
|
+
return res;
|
|
999
|
+
}
|
|
1000
|
+
/** Apply discount code */
|
|
1001
|
+
async applyDiscount(code) {
|
|
1002
|
+
const res = await this.client.request("POST", "/cart/discount", {
|
|
1003
|
+
body: { code }
|
|
1004
|
+
});
|
|
1005
|
+
if (res.error) return res;
|
|
1006
|
+
this.client.emit("cart:updated", res.data);
|
|
1007
|
+
return res;
|
|
1008
|
+
}
|
|
1009
|
+
/** Remove discount code */
|
|
1010
|
+
async removeDiscount() {
|
|
1011
|
+
const res = await this.client.request("DELETE", "/cart/discount");
|
|
1012
|
+
if (res.error) return res;
|
|
1013
|
+
this.client.emit("cart:updated", res.data);
|
|
1014
|
+
return res;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
var CheckoutModule = class {
|
|
1018
|
+
constructor(client) {
|
|
1019
|
+
this.client = client;
|
|
1020
|
+
}
|
|
1021
|
+
/** Create order from cart */
|
|
1022
|
+
async createOrder(input) {
|
|
1023
|
+
const res = await this.client.request("POST", "/checkout", {
|
|
1024
|
+
body: input
|
|
1025
|
+
});
|
|
1026
|
+
if (res.error) return res;
|
|
1027
|
+
this.client.clearCartSession();
|
|
1028
|
+
this.client.emit("order:created", res.data);
|
|
1029
|
+
this.client.emit("cart:cleared");
|
|
1030
|
+
return res;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
var OrdersModule = class {
|
|
1034
|
+
constructor(client) {
|
|
1035
|
+
this.client = client;
|
|
1036
|
+
}
|
|
1037
|
+
/** List customer orders (requires auth) */
|
|
1038
|
+
async list(options) {
|
|
1039
|
+
return this.client.request(
|
|
1040
|
+
"GET",
|
|
1041
|
+
"/orders",
|
|
1042
|
+
{
|
|
1043
|
+
query: { page: options?.page, limit: options?.limit }
|
|
1044
|
+
}
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
/** Get order detail (requires auth) */
|
|
1048
|
+
async get(orderNumber) {
|
|
1049
|
+
return this.client.request("GET", `/orders/${orderNumber}`);
|
|
1050
|
+
}
|
|
1051
|
+
/** Cancel a PENDING order (requires auth) */
|
|
1052
|
+
async cancel(orderNumber) {
|
|
1053
|
+
return this.client.request(
|
|
1054
|
+
"POST",
|
|
1055
|
+
`/orders/${orderNumber}/cancel`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Track an order by tracking token (no login, only API key). Returns a
|
|
1060
|
+
* PII-minimized view: order status + items + masked email + destination
|
|
1061
|
+
* city, never full address / phone / billing — the token travels in URLs
|
|
1062
|
+
* and e-mails so it must not expose full personal data.
|
|
1063
|
+
*/
|
|
1064
|
+
async track(trackingToken) {
|
|
1065
|
+
return this.client.request(
|
|
1066
|
+
"GET",
|
|
1067
|
+
`/orders/track/${trackingToken}`,
|
|
1068
|
+
{ auth: false }
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Step 1 of guest order-access: request a 6-digit code e-mailed to the
|
|
1073
|
+
* address on the order. The response is always generic (success) regardless
|
|
1074
|
+
* of whether the order number + e-mail match, so order numbers can't be
|
|
1075
|
+
* enumerated. No login, only API key.
|
|
1076
|
+
*/
|
|
1077
|
+
async requestAccessCode(orderNumber, email) {
|
|
1078
|
+
return this.client.request(
|
|
1079
|
+
"POST",
|
|
1080
|
+
"/orders/access/request",
|
|
1081
|
+
{
|
|
1082
|
+
auth: false,
|
|
1083
|
+
body: { orderNumber, email }
|
|
1084
|
+
}
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Step 2 of guest order-access: verify the e-mailed code. On success returns
|
|
1089
|
+
* the FULL order detail plus a short-lived `accessToken` you can pass to
|
|
1090
|
+
* {@link getByAccessToken} to re-fetch the detail without re-entering the
|
|
1091
|
+
* code. No login, only API key.
|
|
1092
|
+
*/
|
|
1093
|
+
async verifyAccessCode(orderNumber, email, code) {
|
|
1094
|
+
return this.client.request(
|
|
1095
|
+
"POST",
|
|
1096
|
+
"/orders/access/verify",
|
|
1097
|
+
{
|
|
1098
|
+
auth: false,
|
|
1099
|
+
body: { orderNumber, email, code }
|
|
1100
|
+
}
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Re-fetch a guest order's full detail using the `accessToken` returned by
|
|
1105
|
+
* {@link verifyAccessCode}. The token is scoped to that single order and
|
|
1106
|
+
* expires after 30 minutes.
|
|
1107
|
+
*/
|
|
1108
|
+
async getByAccessToken(accessToken) {
|
|
1109
|
+
return this.client.request("GET", "/orders/access/detail", {
|
|
1110
|
+
auth: false,
|
|
1111
|
+
headers: { "X-Order-Access": accessToken }
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Guest digital download (GAP-07): mint a short-lived signed URL for a
|
|
1116
|
+
* download grant on a guest-accessed order, using the order-access token from
|
|
1117
|
+
* {@link verifyAccessCode}. Scoped to that one order.
|
|
1118
|
+
*/
|
|
1119
|
+
async getAccessDownloadUrl(accessToken, downloadId) {
|
|
1120
|
+
return this.client.request(
|
|
1121
|
+
"POST",
|
|
1122
|
+
`/orders/access/downloads/${downloadId}/url`,
|
|
1123
|
+
{
|
|
1124
|
+
auth: false,
|
|
1125
|
+
headers: { "X-Order-Access": accessToken }
|
|
1126
|
+
}
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
var CustomerModule = class {
|
|
1131
|
+
constructor(client) {
|
|
1132
|
+
this.client = client;
|
|
1133
|
+
}
|
|
1134
|
+
/** Get customer profile */
|
|
1135
|
+
async getProfile() {
|
|
1136
|
+
return this.client.request("GET", "/customer/profile");
|
|
1137
|
+
}
|
|
1138
|
+
/** Update customer profile */
|
|
1139
|
+
async updateProfile(data) {
|
|
1140
|
+
return this.client.request("PATCH", "/customer/profile", {
|
|
1141
|
+
body: data
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
/** Change password */
|
|
1145
|
+
async changePassword(currentPassword, newPassword) {
|
|
1146
|
+
return this.client.request("PUT", "/customer/password", {
|
|
1147
|
+
body: { currentPassword, newPassword }
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
/** List addresses */
|
|
1151
|
+
async getAddresses() {
|
|
1152
|
+
return this.client.request(
|
|
1153
|
+
"GET",
|
|
1154
|
+
"/customer/addresses"
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
/** Create address */
|
|
1158
|
+
async createAddress(address) {
|
|
1159
|
+
return this.client.request("POST", "/customer/addresses", {
|
|
1160
|
+
body: address
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
/** Update address */
|
|
1164
|
+
async updateAddress(addressId, data) {
|
|
1165
|
+
return this.client.request(
|
|
1166
|
+
"PATCH",
|
|
1167
|
+
`/customer/addresses/${addressId}`,
|
|
1168
|
+
{ body: data }
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
/** Delete address */
|
|
1172
|
+
async deleteAddress(addressId) {
|
|
1173
|
+
return this.client.request(
|
|
1174
|
+
"DELETE",
|
|
1175
|
+
`/customer/addresses/${addressId}`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Loyalty program summary for the logged-in customer (points balance +
|
|
1180
|
+
* value, current tier, next-tier progress, point ratio, referral code,
|
|
1181
|
+
* recent transactions). Requires an authenticated customer session.
|
|
1182
|
+
*/
|
|
1183
|
+
async getLoyalty() {
|
|
1184
|
+
return this.client.request("GET", "/customer/loyalty");
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Digital product delivery (GAP-07): list the logged-in customer's download
|
|
1188
|
+
* grants across all their orders (file name, product, remaining downloads,
|
|
1189
|
+
* expiry). Requires an authenticated customer session.
|
|
1190
|
+
*/
|
|
1191
|
+
async getDownloads() {
|
|
1192
|
+
return this.client.request(
|
|
1193
|
+
"GET",
|
|
1194
|
+
"/customer/downloads"
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Mint a short-lived signed URL for one download grant. Counts against the
|
|
1199
|
+
* grant's download budget and enforces the max-download + expiry limits
|
|
1200
|
+
* server-side. Requires an authenticated customer session.
|
|
1201
|
+
*/
|
|
1202
|
+
async getDownloadUrl(downloadId) {
|
|
1203
|
+
return this.client.request(
|
|
1204
|
+
"POST",
|
|
1205
|
+
`/customer/downloads/${downloadId}/url`
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Online courses (LMS): list the logged-in customer's enrolled courses
|
|
1210
|
+
* with progress. Enrollment is created automatically when an order with a
|
|
1211
|
+
* course product is paid. Requires an authenticated customer session.
|
|
1212
|
+
*/
|
|
1213
|
+
async getCourses() {
|
|
1214
|
+
return this.client.request(
|
|
1215
|
+
"GET",
|
|
1216
|
+
"/customer/courses"
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Course player payload: modules and lessons in order, with per-lesson
|
|
1221
|
+
* drip-unlock state. Locked lessons never contain content — the server
|
|
1222
|
+
* withholds `videoUrl`/`content`/`attachments` until `unlockAt`.
|
|
1223
|
+
*/
|
|
1224
|
+
async getCourse(courseId) {
|
|
1225
|
+
return this.client.request(
|
|
1226
|
+
"GET",
|
|
1227
|
+
`/customer/courses/${courseId}`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
/** Mark an unlocked lesson as completed (idempotent). */
|
|
1231
|
+
async completeLesson(courseId, lessonId) {
|
|
1232
|
+
return this.client.request(
|
|
1233
|
+
"POST",
|
|
1234
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/complete`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Quiz for an unlocked lesson. Correct answers are never included — scoring
|
|
1239
|
+
* happens server-side in `submitLessonQuiz`. Locked lessons return 403.
|
|
1240
|
+
*/
|
|
1241
|
+
async getLessonQuiz(courseId, lessonId) {
|
|
1242
|
+
return this.client.request(
|
|
1243
|
+
"GET",
|
|
1244
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/quiz`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Submit quiz answers: returns the score, reveals correct answers and, when
|
|
1249
|
+
* the score reaches `passPercent` (70 %), marks the lesson completed
|
|
1250
|
+
* automatically.
|
|
1251
|
+
*/
|
|
1252
|
+
async submitLessonQuiz(courseId, lessonId, answers) {
|
|
1253
|
+
return this.client.request(
|
|
1254
|
+
"POST",
|
|
1255
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/quiz/submit`,
|
|
1256
|
+
{ body: { answers } }
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Completion certificates of the logged-in customer. Each carries a public
|
|
1261
|
+
* verification code for sharing (LinkedIn, CV).
|
|
1262
|
+
*/
|
|
1263
|
+
async getCourseCertificates() {
|
|
1264
|
+
return this.client.request(
|
|
1265
|
+
"GET",
|
|
1266
|
+
"/customer/courses/certificates"
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* AI tutor "Ask about this lesson": the student's private thread for one
|
|
1271
|
+
* lesson (oldest first). `enabled: false` = the merchant turned the tutor
|
|
1272
|
+
* off — hide the widget. Locked lessons return 403.
|
|
1273
|
+
*/
|
|
1274
|
+
async getLessonTutorThread(courseId, lessonId) {
|
|
1275
|
+
return this.client.request(
|
|
1276
|
+
"GET",
|
|
1277
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/tutor`
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Ask the AI tutor a question about the lesson. The answer sticks to the
|
|
1282
|
+
* lesson topic and comes back in the language of the question (Czech by
|
|
1283
|
+
* default). Rate limited (20/min); 403 on locked lessons or when the
|
|
1284
|
+
* merchant disabled the tutor.
|
|
1285
|
+
*/
|
|
1286
|
+
async askLessonTutor(courseId, lessonId, question) {
|
|
1287
|
+
return this.client.request(
|
|
1288
|
+
"POST",
|
|
1289
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/tutor`,
|
|
1290
|
+
{ body: { question } }
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Lesson discussion: paginated top-level comments (newest first) with one
|
|
1295
|
+
* level of replies. Only enrolled customers with the lesson unlocked; 403
|
|
1296
|
+
* when the merchant disabled the discussion.
|
|
1297
|
+
*/
|
|
1298
|
+
async getLessonComments(courseId, lessonId, page = 1) {
|
|
1299
|
+
return this.client.request(
|
|
1300
|
+
"GET",
|
|
1301
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/comments`,
|
|
1302
|
+
{ query: { page: String(page) } }
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Post a comment under the lesson, or a reply when `parentId` points to a
|
|
1307
|
+
* top-level comment (replies go one level deep only). Max 5000 characters.
|
|
1308
|
+
*/
|
|
1309
|
+
async postLessonComment(courseId, lessonId, body, parentId) {
|
|
1310
|
+
return this.client.request(
|
|
1311
|
+
"POST",
|
|
1312
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/comments`,
|
|
1313
|
+
{ body: { body, ...parentId ? { parentId } : {} } }
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Delete the customer's OWN comment (including its replies). Someone
|
|
1318
|
+
* else's comment returns 404.
|
|
1319
|
+
*/
|
|
1320
|
+
async deleteLessonComment(courseId, lessonId, commentId) {
|
|
1321
|
+
return this.client.request(
|
|
1322
|
+
"DELETE",
|
|
1323
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/comments/${commentId}`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* The student's private note for one lesson. `body` is an empty string
|
|
1328
|
+
* when no note exists yet. Visible only to the logged-in student.
|
|
1329
|
+
*/
|
|
1330
|
+
async getLessonNote(courseId, lessonId) {
|
|
1331
|
+
return this.client.request(
|
|
1332
|
+
"GET",
|
|
1333
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/note`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Save (upsert) the student's private lesson note. Autosave-friendly; an
|
|
1338
|
+
* empty string clears the note. Max 20 000 characters.
|
|
1339
|
+
*/
|
|
1340
|
+
async saveLessonNote(courseId, lessonId, body) {
|
|
1341
|
+
return this.client.request(
|
|
1342
|
+
"PUT",
|
|
1343
|
+
`/customer/courses/${courseId}/lessons/${lessonId}/note`,
|
|
1344
|
+
{ body: { body } }
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
var CourseCertificatesModule = class {
|
|
1349
|
+
constructor(client) {
|
|
1350
|
+
this.client = client;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Publicly verify a certificate code (no customer login needed) — build a
|
|
1354
|
+
* `/certifikat/{code}` page with this. Unknown codes return a 404 error.
|
|
1355
|
+
*/
|
|
1356
|
+
async verify(code) {
|
|
1357
|
+
return this.client.request(
|
|
1358
|
+
"GET",
|
|
1359
|
+
`/course-certificates/${encodeURIComponent(code)}`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Download the certificate as a branded PDF (A5 landscape). Returns a Blob;
|
|
1364
|
+
* trigger a browser download via `URL.createObjectURL(blob)`.
|
|
1365
|
+
*/
|
|
1366
|
+
async downloadPdf(code) {
|
|
1367
|
+
return this.client.requestBlob(
|
|
1368
|
+
`/course-certificates/${encodeURIComponent(code)}/pdf`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
var PagesModule = class {
|
|
1373
|
+
constructor(client) {
|
|
1374
|
+
this.client = client;
|
|
1375
|
+
}
|
|
1376
|
+
/** List CMS pages */
|
|
1377
|
+
async list(locale) {
|
|
1378
|
+
return this.client.request("GET", "/pages", {
|
|
1379
|
+
query: { locale }
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
/** Get page by slug */
|
|
1383
|
+
async get(slug, locale) {
|
|
1384
|
+
return this.client.request("GET", `/pages/${slug}`, {
|
|
1385
|
+
query: { locale }
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
var WishlistModule = class {
|
|
1390
|
+
constructor(client) {
|
|
1391
|
+
this.client = client;
|
|
1392
|
+
}
|
|
1393
|
+
async get() {
|
|
1394
|
+
return this.client.request(
|
|
1395
|
+
"GET",
|
|
1396
|
+
"/customer/wishlist"
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
async add(productId) {
|
|
1400
|
+
return this.client.request(
|
|
1401
|
+
"POST",
|
|
1402
|
+
"/customer/wishlist",
|
|
1403
|
+
{ body: { productId } }
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
async remove(productId) {
|
|
1407
|
+
return this.client.request(
|
|
1408
|
+
"DELETE",
|
|
1409
|
+
`/customer/wishlist/${productId}`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
async isInWishlist(productId) {
|
|
1413
|
+
return this.client.request(
|
|
1414
|
+
"GET",
|
|
1415
|
+
`/customer/wishlist/${productId}/check`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
var SubscriptionsModule = class {
|
|
1420
|
+
constructor(client) {
|
|
1421
|
+
this.client = client;
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* List the logged-in customer's recurring-order subscriptions (products,
|
|
1425
|
+
* cadence, next order date, status). Requires an authenticated customer
|
|
1426
|
+
* session. Subscriptions are created by the merchant in v1.
|
|
1427
|
+
*/
|
|
1428
|
+
async list() {
|
|
1429
|
+
return this.client.request(
|
|
1430
|
+
"GET",
|
|
1431
|
+
"/customer/subscriptions"
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
/** Pause an active subscription (no orders are generated while paused). */
|
|
1435
|
+
async pause(subscriptionId) {
|
|
1436
|
+
return this.client.request(
|
|
1437
|
+
"POST",
|
|
1438
|
+
`/customer/subscriptions/${subscriptionId}/pause`
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
/** Resume a paused subscription (re-schedules the next order). */
|
|
1442
|
+
async resume(subscriptionId) {
|
|
1443
|
+
return this.client.request(
|
|
1444
|
+
"POST",
|
|
1445
|
+
`/customer/subscriptions/${subscriptionId}/resume`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
/** Cancel a subscription permanently (no more orders). */
|
|
1449
|
+
async cancel(subscriptionId) {
|
|
1450
|
+
return this.client.request(
|
|
1451
|
+
"POST",
|
|
1452
|
+
`/customer/subscriptions/${subscriptionId}/cancel`
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
var ReviewsModule = class {
|
|
1457
|
+
constructor(client) {
|
|
1458
|
+
this.client = client;
|
|
1459
|
+
}
|
|
1460
|
+
async getProductReviews(productId, page = 1, limit = 20) {
|
|
1461
|
+
return this.client.request(
|
|
1462
|
+
"GET",
|
|
1463
|
+
`/catalog/products/${productId}/reviews`,
|
|
1464
|
+
{
|
|
1465
|
+
query: { page: String(page), limit: String(limit) }
|
|
1466
|
+
}
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
async submit(input) {
|
|
1470
|
+
return this.client.request("POST", "/catalog/reviews", {
|
|
1471
|
+
body: input
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
async voteHelpful(reviewId, helpful) {
|
|
1475
|
+
return this.client.request(
|
|
1476
|
+
"POST",
|
|
1477
|
+
`/catalog/reviews/${reviewId}/vote`,
|
|
1478
|
+
{ body: { helpful } }
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
var ReturnsModule = class {
|
|
1483
|
+
constructor(client) {
|
|
1484
|
+
this.client = client;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Guest order lookup for the EU withdrawal form: order number + the email
|
|
1488
|
+
* used on the order resolve to the order id and per-item returnable
|
|
1489
|
+
* quantities. POST so the email never appears in a URL.
|
|
1490
|
+
*/
|
|
1491
|
+
async lookupOrder(orderNumber, email) {
|
|
1492
|
+
return this.client.request(
|
|
1493
|
+
"POST",
|
|
1494
|
+
"/returns/lookup-order",
|
|
1495
|
+
{
|
|
1496
|
+
body: { orderNumber, email }
|
|
1497
|
+
}
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
async submit(input) {
|
|
1501
|
+
return this.client.request("POST", "/returns", {
|
|
1502
|
+
body: input
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
/** Email is the ownership gate; POST so it never lands in a URL / log. */
|
|
1506
|
+
async getStatus(returnId, email) {
|
|
1507
|
+
return this.client.request(
|
|
1508
|
+
"POST",
|
|
1509
|
+
`/returns/${returnId}/status`,
|
|
1510
|
+
{ body: { email } }
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
var ConsentModule = class {
|
|
1515
|
+
constructor(client) {
|
|
1516
|
+
this.client = client;
|
|
1517
|
+
}
|
|
1518
|
+
async record(input) {
|
|
1519
|
+
return this.client.request("POST", "/consent", {
|
|
1520
|
+
body: input,
|
|
1521
|
+
auth: false
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
async get(visitorId) {
|
|
1525
|
+
const result = await this.client.request("GET", `/consent/${visitorId}/status`, { auth: false });
|
|
1526
|
+
if (result.error && result.error.status === 404) {
|
|
1527
|
+
const legacy = await this.client.request(
|
|
1528
|
+
"GET",
|
|
1529
|
+
`/consent/${visitorId}`,
|
|
1530
|
+
{ auth: false }
|
|
1531
|
+
);
|
|
1532
|
+
if (legacy.error && legacy.error.status === 404)
|
|
1533
|
+
return { data: null, error: null };
|
|
1534
|
+
return legacy;
|
|
1535
|
+
}
|
|
1536
|
+
if (result.error) return { data: null, error: result.error };
|
|
1537
|
+
return {
|
|
1538
|
+
data: result.data?.consented ? result.data.consent : null,
|
|
1539
|
+
error: null
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
async revoke(visitorId) {
|
|
1543
|
+
return this.client.request(
|
|
1544
|
+
"DELETE",
|
|
1545
|
+
`/consent/${visitorId}`,
|
|
1546
|
+
{ auth: false }
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
var QuotesModule = class {
|
|
1551
|
+
constructor(client) {
|
|
1552
|
+
this.client = client;
|
|
1553
|
+
}
|
|
1554
|
+
async submit(input) {
|
|
1555
|
+
return this.client.request("POST", "/catalog/quote-request", {
|
|
1556
|
+
body: input
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
async accept(quoteId, email) {
|
|
1560
|
+
return this.client.request(
|
|
1561
|
+
"POST",
|
|
1562
|
+
`/quotes/${quoteId}/accept`,
|
|
1563
|
+
{ body: { email } }
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
/** Email is the ownership gate — quotes carry contact PII and negotiated
|
|
1567
|
+
* prices, so the id alone is never enough. POST keeps it out of URLs. */
|
|
1568
|
+
async getStatus(quoteId, email) {
|
|
1569
|
+
return this.client.request(
|
|
1570
|
+
"POST",
|
|
1571
|
+
`/quotes/${quoteId}/status`,
|
|
1572
|
+
{ body: { email } }
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* The logged-in customer's own quote requests ("Moje poptávky", GAP-18).
|
|
1577
|
+
* Requires an authenticated session; ownership is the auth token (customer id
|
|
1578
|
+
* + verified email), never a payload. Newest first.
|
|
1579
|
+
*/
|
|
1580
|
+
async listMine() {
|
|
1581
|
+
return this.client.request(
|
|
1582
|
+
"GET",
|
|
1583
|
+
"/customer/quotes"
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
var AddressModule = class {
|
|
1588
|
+
constructor(client) {
|
|
1589
|
+
this.client = client;
|
|
1590
|
+
}
|
|
1591
|
+
/** Search for address suggestions (debounce on your side, or use the React hook) */
|
|
1592
|
+
async autocomplete(query, country) {
|
|
1593
|
+
if (!query || query.length < 2) return ok({ suggestions: [] });
|
|
1594
|
+
return this.client.request(
|
|
1595
|
+
"GET",
|
|
1596
|
+
"/addresses/autocomplete",
|
|
1597
|
+
{
|
|
1598
|
+
query: { q: query, country }
|
|
1599
|
+
}
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
/** Get full structured address from a suggestion's placeId */
|
|
1603
|
+
async getDetail(placeId) {
|
|
1604
|
+
return this.client.request(
|
|
1605
|
+
"GET",
|
|
1606
|
+
"/addresses/place-detail",
|
|
1607
|
+
{
|
|
1608
|
+
query: { placeId }
|
|
1609
|
+
}
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
var ShippingModule = class {
|
|
1614
|
+
constructor(client) {
|
|
1615
|
+
this.client = client;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Return the configured shipping methods that pass the current
|
|
1619
|
+
* currency + country filter. Fixed-price methods come back with
|
|
1620
|
+
* their `pricing[]` row resolved; live-quote methods come back with
|
|
1621
|
+
* `price` 0 here — call `quote()` to get the real live price.
|
|
1622
|
+
*/
|
|
1623
|
+
async listMethods(opts) {
|
|
1624
|
+
const query = {};
|
|
1625
|
+
if (opts?.currency) query.currency = opts.currency;
|
|
1626
|
+
if (opts?.country) query.country = opts.country;
|
|
1627
|
+
if (opts?.cartTotal != null) query.cartTotal = String(opts.cartTotal);
|
|
1628
|
+
if (opts?.cartWeightKg != null)
|
|
1629
|
+
query.cartWeightKg = String(opts.cartWeightKg);
|
|
1630
|
+
return this.client.request(
|
|
1631
|
+
"GET",
|
|
1632
|
+
"/catalog/shipping-methods",
|
|
1633
|
+
{ query }
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Quote shipping for a destination address + cart contents. Each
|
|
1638
|
+
* configured method is evaluated:
|
|
1639
|
+
* - `priceStrategy="fixed"` → resolved from the merchant's per-currency
|
|
1640
|
+
* `pricing[]` rows + free-shipping threshold check.
|
|
1641
|
+
* - `priceStrategy="live_quote"` → dispatched to the upstream
|
|
1642
|
+
* meta-provider (Zaslat, future Shippo / Sendcloud / …) and run
|
|
1643
|
+
* through the merchant's markup/rounding rules.
|
|
1644
|
+
*
|
|
1645
|
+
* Filter `available: true` for the checkout picker; `available: false`
|
|
1646
|
+
* rows carry a `reason` (`"no_rate_returned"`, `"live_quote_not_implemented"`,
|
|
1647
|
+
* …) you can log but should not display.
|
|
1648
|
+
*/
|
|
1649
|
+
async quote(input) {
|
|
1650
|
+
return this.client.request(
|
|
1651
|
+
"POST",
|
|
1652
|
+
"/catalog/shipping/quote",
|
|
1653
|
+
{ body: input }
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* List pickup points (parcel shops / lockers) for a method that has
|
|
1658
|
+
* `supportsPickupPoints`. Filter with `query` (name / city / zip) for a
|
|
1659
|
+
* "find your branch" box. Send the chosen point's `externalId` back as
|
|
1660
|
+
* `checkout.pickupPointId`.
|
|
1661
|
+
*/
|
|
1662
|
+
async getPickupPoints(input) {
|
|
1663
|
+
const query = { methodId: input.methodId };
|
|
1664
|
+
if (input.query) query.query = input.query;
|
|
1665
|
+
if (input.country) query.country = input.country;
|
|
1666
|
+
if (input.limit != null) query.limit = String(input.limit);
|
|
1667
|
+
return this.client.request(
|
|
1668
|
+
"GET",
|
|
1669
|
+
"/catalog/shipping/pickup-points",
|
|
1670
|
+
{ query }
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
var NewsletterModule = class {
|
|
1675
|
+
constructor(client) {
|
|
1676
|
+
this.client = client;
|
|
1677
|
+
}
|
|
1678
|
+
async subscribe(input) {
|
|
1679
|
+
return this.client.request(
|
|
1680
|
+
"POST",
|
|
1681
|
+
"/newsletter/subscribe",
|
|
1682
|
+
{
|
|
1683
|
+
body: input,
|
|
1684
|
+
auth: false
|
|
1685
|
+
}
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
async unsubscribe(email) {
|
|
1689
|
+
return this.client.request(
|
|
1690
|
+
"POST",
|
|
1691
|
+
"/newsletter/unsubscribe",
|
|
1692
|
+
{
|
|
1693
|
+
body: { email },
|
|
1694
|
+
auth: false
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
|
|
17
1700
|
// src/react/context.ts
|
|
18
1701
|
import { createContext, useContext } from "react";
|
|
19
1702
|
var BehioContext = createContext(null);
|
|
@@ -935,28 +2618,233 @@ function useCourse(courseId, options) {
|
|
|
935
2618
|
isCompleting: complete.isPending
|
|
936
2619
|
};
|
|
937
2620
|
}
|
|
2621
|
+
function useLessonQuiz(courseId, lessonId, options) {
|
|
2622
|
+
const { client } = useBehio();
|
|
2623
|
+
const queryClient = useQueryClient7();
|
|
2624
|
+
const { data, isLoading, error, refetch } = useQuery16({
|
|
2625
|
+
queryKey: [...COURSES_KEY, courseId, "quiz", lessonId],
|
|
2626
|
+
queryFn: () => unwrap(
|
|
2627
|
+
client.customer.getLessonQuiz(courseId, lessonId)
|
|
2628
|
+
),
|
|
2629
|
+
enabled: options?.enabled !== false && !!courseId && !!lessonId && !!client.getAccessToken()
|
|
2630
|
+
});
|
|
2631
|
+
const submit = useMutation5(
|
|
2632
|
+
{
|
|
2633
|
+
mutationFn: ({ answers }) => unwrap(
|
|
2634
|
+
client.customer.submitLessonQuiz(
|
|
2635
|
+
courseId,
|
|
2636
|
+
lessonId,
|
|
2637
|
+
answers
|
|
2638
|
+
)
|
|
2639
|
+
),
|
|
2640
|
+
onSuccess: (result) => {
|
|
2641
|
+
if (result.lessonCompleted) {
|
|
2642
|
+
void queryClient.invalidateQueries({ queryKey: [...COURSES_KEY] });
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
);
|
|
2647
|
+
return {
|
|
2648
|
+
quiz: data,
|
|
2649
|
+
isLoading,
|
|
2650
|
+
error,
|
|
2651
|
+
refetch,
|
|
2652
|
+
submitQuiz: (answers) => submit.mutateAsync({ answers }),
|
|
2653
|
+
isSubmitting: submit.isPending,
|
|
2654
|
+
result: submit.data ?? null
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
function useCourseCertificates(options) {
|
|
2658
|
+
const { client } = useBehio();
|
|
2659
|
+
const { data, isLoading, error, refetch } = useQuery16({
|
|
2660
|
+
queryKey: [...COURSES_KEY, "certificates"],
|
|
2661
|
+
queryFn: () => unwrap(client.customer.getCourseCertificates()),
|
|
2662
|
+
enabled: options?.enabled !== false && !!client.getAccessToken()
|
|
2663
|
+
});
|
|
2664
|
+
return { certificates: data?.items ?? [], isLoading, error, refetch };
|
|
2665
|
+
}
|
|
2666
|
+
function useCertificateVerification(code, options) {
|
|
2667
|
+
const { client } = useBehio();
|
|
2668
|
+
const { data, isLoading, error, refetch } = useQuery16(
|
|
2669
|
+
{
|
|
2670
|
+
queryKey: ["behio", "certificate-verification", code],
|
|
2671
|
+
queryFn: () => unwrap(client.certificates.verify(code)),
|
|
2672
|
+
enabled: options?.enabled !== false && !!code,
|
|
2673
|
+
retry: false
|
|
2674
|
+
}
|
|
2675
|
+
);
|
|
2676
|
+
return { verification: data ?? null, isLoading, error, refetch };
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/react/hooks/use-lesson-tutor.ts
|
|
2680
|
+
import { useMutation as useMutation6, useQuery as useQuery17, useQueryClient as useQueryClient8 } from "@tanstack/react-query";
|
|
2681
|
+
var TUTOR_KEY = ["behio", "course-tutor"];
|
|
2682
|
+
function useLessonTutor(courseId, lessonId, options) {
|
|
2683
|
+
const { client } = useBehio();
|
|
2684
|
+
const queryClient = useQueryClient8();
|
|
2685
|
+
const { data, isLoading, error, refetch } = useQuery17({
|
|
2686
|
+
queryKey: [...TUTOR_KEY, courseId, lessonId],
|
|
2687
|
+
queryFn: () => unwrap(
|
|
2688
|
+
client.customer.getLessonTutorThread(
|
|
2689
|
+
courseId,
|
|
2690
|
+
lessonId
|
|
2691
|
+
)
|
|
2692
|
+
),
|
|
2693
|
+
enabled: options?.enabled !== false && !!courseId && !!lessonId && !!client.getAccessToken()
|
|
2694
|
+
});
|
|
2695
|
+
const ask = useMutation6({
|
|
2696
|
+
mutationFn: ({ question }) => unwrap(
|
|
2697
|
+
client.customer.askLessonTutor(
|
|
2698
|
+
courseId,
|
|
2699
|
+
lessonId,
|
|
2700
|
+
question
|
|
2701
|
+
)
|
|
2702
|
+
),
|
|
2703
|
+
onSuccess: (message) => {
|
|
2704
|
+
queryClient.setQueryData(
|
|
2705
|
+
[...TUTOR_KEY, courseId, lessonId],
|
|
2706
|
+
(prev) => prev ? { ...prev, items: [...prev.items, message] } : prev
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2710
|
+
return {
|
|
2711
|
+
messages: data?.items ?? [],
|
|
2712
|
+
/** False when the merchant disabled the AI tutor — hide the widget. */
|
|
2713
|
+
enabled: data?.enabled ?? true,
|
|
2714
|
+
isLoading,
|
|
2715
|
+
error,
|
|
2716
|
+
refetch,
|
|
2717
|
+
ask: (question) => ask.mutateAsync({ question }),
|
|
2718
|
+
isAsking: ask.isPending,
|
|
2719
|
+
askError: ask.error
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// src/react/hooks/use-lesson-comments.ts
|
|
2724
|
+
import { useMutation as useMutation7, useQuery as useQuery18, useQueryClient as useQueryClient9 } from "@tanstack/react-query";
|
|
2725
|
+
var COMMENTS_KEY = ["behio", "course-comments"];
|
|
2726
|
+
function useLessonComments(courseId, lessonId, options) {
|
|
2727
|
+
const { client } = useBehio();
|
|
2728
|
+
const queryClient = useQueryClient9();
|
|
2729
|
+
const page = options?.page ?? 1;
|
|
2730
|
+
const { data, isLoading, error, refetch } = useQuery18({
|
|
2731
|
+
queryKey: [...COMMENTS_KEY, courseId, lessonId, page],
|
|
2732
|
+
queryFn: () => unwrap(
|
|
2733
|
+
client.customer.getLessonComments(
|
|
2734
|
+
courseId,
|
|
2735
|
+
lessonId,
|
|
2736
|
+
page
|
|
2737
|
+
)
|
|
2738
|
+
),
|
|
2739
|
+
enabled: options?.enabled !== false && !!courseId && !!lessonId && !!client.getAccessToken()
|
|
2740
|
+
});
|
|
2741
|
+
const invalidate = () => queryClient.invalidateQueries({
|
|
2742
|
+
queryKey: [...COMMENTS_KEY, courseId, lessonId]
|
|
2743
|
+
});
|
|
2744
|
+
const post = useMutation7({
|
|
2745
|
+
mutationFn: ({ body, parentId }) => unwrap(
|
|
2746
|
+
client.customer.postLessonComment(
|
|
2747
|
+
courseId,
|
|
2748
|
+
lessonId,
|
|
2749
|
+
body,
|
|
2750
|
+
parentId
|
|
2751
|
+
)
|
|
2752
|
+
),
|
|
2753
|
+
onSuccess: () => {
|
|
2754
|
+
void invalidate();
|
|
2755
|
+
}
|
|
2756
|
+
});
|
|
2757
|
+
const remove = useMutation7({
|
|
2758
|
+
mutationFn: ({ commentId }) => unwrap(
|
|
2759
|
+
client.customer.deleteLessonComment(
|
|
2760
|
+
courseId,
|
|
2761
|
+
lessonId,
|
|
2762
|
+
commentId
|
|
2763
|
+
)
|
|
2764
|
+
),
|
|
2765
|
+
onSuccess: () => {
|
|
2766
|
+
void invalidate();
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
return {
|
|
2770
|
+
comments: data?.items ?? [],
|
|
2771
|
+
totalCount: data?.totalCount ?? 0,
|
|
2772
|
+
page: data?.page ?? page,
|
|
2773
|
+
pageSize: data?.pageSize ?? 20,
|
|
2774
|
+
/** False when the merchant disabled the discussion — hide the widget. */
|
|
2775
|
+
enabled: data?.enabled ?? true,
|
|
2776
|
+
isLoading,
|
|
2777
|
+
error,
|
|
2778
|
+
refetch,
|
|
2779
|
+
postComment: (body, parentId) => post.mutateAsync({ body, parentId }),
|
|
2780
|
+
isPosting: post.isPending,
|
|
2781
|
+
deleteComment: (commentId) => remove.mutateAsync({ commentId }),
|
|
2782
|
+
isDeleting: remove.isPending
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// src/react/hooks/use-lesson-note.ts
|
|
2787
|
+
import { useMutation as useMutation8, useQuery as useQuery19, useQueryClient as useQueryClient10 } from "@tanstack/react-query";
|
|
2788
|
+
var NOTE_KEY = ["behio", "course-note"];
|
|
2789
|
+
function useLessonNote(courseId, lessonId, options) {
|
|
2790
|
+
const { client } = useBehio();
|
|
2791
|
+
const queryClient = useQueryClient10();
|
|
2792
|
+
const { data, isLoading, error, refetch } = useQuery19({
|
|
2793
|
+
queryKey: [...NOTE_KEY, courseId, lessonId],
|
|
2794
|
+
queryFn: () => unwrap(
|
|
2795
|
+
client.customer.getLessonNote(courseId, lessonId)
|
|
2796
|
+
),
|
|
2797
|
+
enabled: options?.enabled !== false && !!courseId && !!lessonId && !!client.getAccessToken()
|
|
2798
|
+
});
|
|
2799
|
+
const save = useMutation8({
|
|
2800
|
+
mutationFn: ({ body }) => unwrap(
|
|
2801
|
+
client.customer.saveLessonNote(
|
|
2802
|
+
courseId,
|
|
2803
|
+
lessonId,
|
|
2804
|
+
body
|
|
2805
|
+
)
|
|
2806
|
+
),
|
|
2807
|
+
onSuccess: (note) => {
|
|
2808
|
+
queryClient.setQueryData(
|
|
2809
|
+
[...NOTE_KEY, courseId, lessonId],
|
|
2810
|
+
note
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
return {
|
|
2815
|
+
note: data ?? null,
|
|
2816
|
+
body: data?.body ?? "",
|
|
2817
|
+
updatedAt: data?.updatedAt ?? null,
|
|
2818
|
+
isLoading,
|
|
2819
|
+
error,
|
|
2820
|
+
refetch,
|
|
2821
|
+
save: (body) => save.mutateAsync({ body }),
|
|
2822
|
+
isSaving: save.isPending,
|
|
2823
|
+
saveError: save.error
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
938
2826
|
|
|
939
2827
|
// src/react/hooks/use-subscriptions.ts
|
|
940
|
-
import { useQuery as
|
|
2828
|
+
import { useQuery as useQuery20, useMutation as useMutation9, useQueryClient as useQueryClient11 } from "@tanstack/react-query";
|
|
941
2829
|
var SUBSCRIPTIONS_KEY = ["behio", "subscriptions"];
|
|
942
2830
|
function useSubscriptions(options) {
|
|
943
2831
|
const { client } = useBehio();
|
|
944
|
-
const qc =
|
|
945
|
-
const query =
|
|
2832
|
+
const qc = useQueryClient11();
|
|
2833
|
+
const query = useQuery20({
|
|
946
2834
|
queryKey: [...SUBSCRIPTIONS_KEY],
|
|
947
2835
|
queryFn: () => unwrap(client.subscriptions.list()),
|
|
948
2836
|
enabled: options?.enabled !== false && !!client.getAccessToken()
|
|
949
2837
|
});
|
|
950
2838
|
const invalidate = () => qc.invalidateQueries({ queryKey: [...SUBSCRIPTIONS_KEY] });
|
|
951
|
-
const pauseMutation =
|
|
2839
|
+
const pauseMutation = useMutation9({
|
|
952
2840
|
mutationFn: (subscriptionId) => unwrap(client.subscriptions.pause(subscriptionId)),
|
|
953
2841
|
onSuccess: invalidate
|
|
954
2842
|
});
|
|
955
|
-
const resumeMutation =
|
|
2843
|
+
const resumeMutation = useMutation9({
|
|
956
2844
|
mutationFn: (subscriptionId) => unwrap(client.subscriptions.resume(subscriptionId)),
|
|
957
2845
|
onSuccess: invalidate
|
|
958
2846
|
});
|
|
959
|
-
const cancelMutation =
|
|
2847
|
+
const cancelMutation = useMutation9({
|
|
960
2848
|
mutationFn: (subscriptionId) => unwrap(client.subscriptions.cancel(subscriptionId)),
|
|
961
2849
|
onSuccess: invalidate
|
|
962
2850
|
});
|
|
@@ -975,11 +2863,11 @@ function useSubscriptions(options) {
|
|
|
975
2863
|
}
|
|
976
2864
|
|
|
977
2865
|
// src/react/hooks/use-pickup-points.ts
|
|
978
|
-
import { useQuery as
|
|
2866
|
+
import { useQuery as useQuery21 } from "@tanstack/react-query";
|
|
979
2867
|
function usePickupPoints(input) {
|
|
980
2868
|
const { client } = useBehio();
|
|
981
2869
|
const { methodId, query, country, limit, enabled } = input;
|
|
982
|
-
const { data, isLoading, error, refetch } =
|
|
2870
|
+
const { data, isLoading, error, refetch } = useQuery21({
|
|
983
2871
|
queryKey: ["behio", "pickup-points", methodId, query ?? "", country ?? "", limit ?? 30],
|
|
984
2872
|
queryFn: () => unwrap(
|
|
985
2873
|
client.shipping.getPickupPoints({
|
|
@@ -995,12 +2883,12 @@ function usePickupPoints(input) {
|
|
|
995
2883
|
}
|
|
996
2884
|
|
|
997
2885
|
// src/react/hooks/use-shipping-methods.ts
|
|
998
|
-
import { useQuery as
|
|
2886
|
+
import { useQuery as useQuery22 } from "@tanstack/react-query";
|
|
999
2887
|
function useShippingMethods(options) {
|
|
1000
2888
|
const { client, currency: activeCurrency } = useBehio();
|
|
1001
2889
|
const currency = options?.currency ?? activeCurrency;
|
|
1002
2890
|
const { country, cartTotal, cartWeightKg } = options ?? {};
|
|
1003
|
-
const { data, isLoading, error, refetch } =
|
|
2891
|
+
const { data, isLoading, error, refetch } = useQuery22({
|
|
1004
2892
|
queryKey: [
|
|
1005
2893
|
"behio",
|
|
1006
2894
|
"shipping-methods",
|
|
@@ -1016,12 +2904,12 @@ function useShippingMethods(options) {
|
|
|
1016
2904
|
}
|
|
1017
2905
|
|
|
1018
2906
|
// src/react/hooks/use-shipping-quote.ts
|
|
1019
|
-
import { useQuery as
|
|
2907
|
+
import { useQuery as useQuery23 } from "@tanstack/react-query";
|
|
1020
2908
|
function useShippingQuote(options) {
|
|
1021
2909
|
const { client, currency: activeCurrency } = useBehio();
|
|
1022
2910
|
const { destinationAddress, items, cartTotal, enabled } = options ?? {};
|
|
1023
2911
|
const currency = options?.currency ?? activeCurrency;
|
|
1024
|
-
const { data, isLoading, error, refetch } =
|
|
2912
|
+
const { data, isLoading, error, refetch } = useQuery23({
|
|
1025
2913
|
queryKey: [
|
|
1026
2914
|
"behio",
|
|
1027
2915
|
"shipping-quote",
|
|
@@ -1044,11 +2932,11 @@ function useShippingQuote(options) {
|
|
|
1044
2932
|
}
|
|
1045
2933
|
|
|
1046
2934
|
// src/react/hooks/use-payment-methods.ts
|
|
1047
|
-
import { useQuery as
|
|
2935
|
+
import { useQuery as useQuery24 } from "@tanstack/react-query";
|
|
1048
2936
|
function usePaymentMethods(options) {
|
|
1049
2937
|
const { client, currency: activeCurrency } = useBehio();
|
|
1050
2938
|
const currency = options?.currency ?? activeCurrency;
|
|
1051
|
-
const { data, isLoading, error, refetch } =
|
|
2939
|
+
const { data, isLoading, error, refetch } = useQuery24({
|
|
1052
2940
|
queryKey: ["behio", "payment-methods", currency ?? ""],
|
|
1053
2941
|
queryFn: () => unwrap(client.catalog.listPaymentMethods({ currency })),
|
|
1054
2942
|
enabled: options?.enabled !== false
|
|
@@ -1058,10 +2946,10 @@ function usePaymentMethods(options) {
|
|
|
1058
2946
|
|
|
1059
2947
|
// src/react/hooks/use-personal-offers.ts
|
|
1060
2948
|
import { useCallback as useCallback8, useEffect as useEffect4, useState as useState5 } from "react";
|
|
1061
|
-
import { useMutation as
|
|
2949
|
+
import { useMutation as useMutation10, useQuery as useQuery25, useQueryClient as useQueryClient12 } from "@tanstack/react-query";
|
|
1062
2950
|
function usePersonalOffers(options) {
|
|
1063
2951
|
const { client } = useBehio();
|
|
1064
|
-
const queryClient =
|
|
2952
|
+
const queryClient = useQueryClient12();
|
|
1065
2953
|
const [polledVid, setPolledVid] = useState5(null);
|
|
1066
2954
|
const explicitVid = options?.visitorId;
|
|
1067
2955
|
useEffect4(() => {
|
|
@@ -1079,12 +2967,12 @@ function usePersonalOffers(options) {
|
|
|
1079
2967
|
}, [client, explicitVid]);
|
|
1080
2968
|
const visitorId = explicitVid ?? polledVid;
|
|
1081
2969
|
const queryKey = ["behio", "personal-offers", visitorId ?? ""];
|
|
1082
|
-
const { data, isLoading, error, refetch } =
|
|
2970
|
+
const { data, isLoading, error, refetch } = useQuery25({
|
|
1083
2971
|
queryKey,
|
|
1084
2972
|
queryFn: () => unwrap(client.getPersonalOffers(visitorId)),
|
|
1085
2973
|
enabled: options?.enabled !== false && !!visitorId
|
|
1086
2974
|
});
|
|
1087
|
-
const claimMutation =
|
|
2975
|
+
const claimMutation = useMutation10({
|
|
1088
2976
|
mutationFn: ({ offerId, email }) => unwrap(client.claimOfferByEmail(offerId, { visitorId, email })),
|
|
1089
2977
|
onSuccess: (result, { offerId }) => {
|
|
1090
2978
|
queryClient.setQueryData(
|
|
@@ -1146,16 +3034,16 @@ function useAnalyticsEvents() {
|
|
|
1146
3034
|
}
|
|
1147
3035
|
|
|
1148
3036
|
// src/react/hooks/use-newsletter.ts
|
|
1149
|
-
import { useMutation as
|
|
3037
|
+
import { useMutation as useMutation11 } from "@tanstack/react-query";
|
|
1150
3038
|
function useNewsletterSubscribe() {
|
|
1151
3039
|
const { client } = useBehio();
|
|
1152
|
-
return
|
|
3040
|
+
return useMutation11({
|
|
1153
3041
|
mutationFn: (input) => unwrap(client.newsletter.subscribe(input))
|
|
1154
3042
|
});
|
|
1155
3043
|
}
|
|
1156
3044
|
function useNewsletterUnsubscribe() {
|
|
1157
3045
|
const { client } = useBehio();
|
|
1158
|
-
return
|
|
3046
|
+
return useMutation11({
|
|
1159
3047
|
mutationFn: (email) => unwrap(client.newsletter.unsubscribe(email))
|
|
1160
3048
|
});
|
|
1161
3049
|
}
|
|
@@ -1213,20 +3101,20 @@ function useOrders(options) {
|
|
|
1213
3101
|
|
|
1214
3102
|
// src/react/hooks/use-order.ts
|
|
1215
3103
|
import { useCallback as useCallback11 } from "react";
|
|
1216
|
-
import { useQuery as
|
|
3104
|
+
import { useQuery as useQuery26, useMutation as useMutation12, useQueryClient as useQueryClient13 } from "@tanstack/react-query";
|
|
1217
3105
|
function useOrder(orderNumber, options) {
|
|
1218
3106
|
const { client } = useBehio();
|
|
1219
|
-
const queryClient =
|
|
3107
|
+
const queryClient = useQueryClient13();
|
|
1220
3108
|
const {
|
|
1221
3109
|
data,
|
|
1222
3110
|
isLoading,
|
|
1223
3111
|
error
|
|
1224
|
-
} =
|
|
3112
|
+
} = useQuery26({
|
|
1225
3113
|
queryKey: ["behio", "order", orderNumber],
|
|
1226
3114
|
queryFn: () => unwrap(client.orders.get(orderNumber)),
|
|
1227
3115
|
enabled: options?.enabled !== false && !!orderNumber && !!client.getAccessToken()
|
|
1228
3116
|
});
|
|
1229
|
-
const cancelMutation =
|
|
3117
|
+
const cancelMutation = useMutation12({
|
|
1230
3118
|
mutationFn: () => unwrap(client.orders.cancel(orderNumber)),
|
|
1231
3119
|
onSuccess: (updated) => {
|
|
1232
3120
|
queryClient.setQueryData(["behio", "order", orderNumber], updated);
|
|
@@ -1248,21 +3136,21 @@ function useOrder(orderNumber, options) {
|
|
|
1248
3136
|
|
|
1249
3137
|
// src/react/hooks/use-order-access.ts
|
|
1250
3138
|
import { useCallback as useCallback12, useState as useState7 } from "react";
|
|
1251
|
-
import { useMutation as
|
|
3139
|
+
import { useMutation as useMutation13 } from "@tanstack/react-query";
|
|
1252
3140
|
function useOrderAccess() {
|
|
1253
3141
|
const { client } = useBehio();
|
|
1254
3142
|
const [orderNumber, setOrderNumber] = useState7(null);
|
|
1255
3143
|
const [email, setEmail] = useState7(null);
|
|
1256
3144
|
const [order, setOrder] = useState7(null);
|
|
1257
3145
|
const [accessToken, setAccessToken] = useState7(null);
|
|
1258
|
-
const requestMutation =
|
|
3146
|
+
const requestMutation = useMutation13({
|
|
1259
3147
|
mutationFn: (input) => unwrap(client.orders.requestAccessCode(input.orderNumber, input.email)),
|
|
1260
3148
|
onSuccess: (_data, input) => {
|
|
1261
3149
|
setOrderNumber(input.orderNumber);
|
|
1262
3150
|
setEmail(input.email);
|
|
1263
3151
|
}
|
|
1264
3152
|
});
|
|
1265
|
-
const verifyMutation =
|
|
3153
|
+
const verifyMutation = useMutation13({
|
|
1266
3154
|
mutationFn: (code) => {
|
|
1267
3155
|
if (!orderNumber || !email) {
|
|
1268
3156
|
throw new Error("Request a code before verifying");
|
|
@@ -1308,12 +3196,12 @@ function useOrderAccess() {
|
|
|
1308
3196
|
|
|
1309
3197
|
// src/react/hooks/use-checkout.ts
|
|
1310
3198
|
import { useState as useState8, useCallback as useCallback13 } from "react";
|
|
1311
|
-
import { useMutation as
|
|
3199
|
+
import { useMutation as useMutation14, useQueryClient as useQueryClient14 } from "@tanstack/react-query";
|
|
1312
3200
|
function useCheckout() {
|
|
1313
3201
|
const { client, storage } = useBehio();
|
|
1314
|
-
const queryClient =
|
|
3202
|
+
const queryClient = useQueryClient14();
|
|
1315
3203
|
const [order, setOrder] = useState8(null);
|
|
1316
|
-
const mutation =
|
|
3204
|
+
const mutation = useMutation14({
|
|
1317
3205
|
mutationFn: (input) => unwrap(client.checkout.createOrder(input)),
|
|
1318
3206
|
onSuccess: (result) => {
|
|
1319
3207
|
setOrder(result);
|
|
@@ -1340,10 +3228,10 @@ function useCheckout() {
|
|
|
1340
3228
|
}
|
|
1341
3229
|
|
|
1342
3230
|
// src/react/hooks/use-pages.ts
|
|
1343
|
-
import { useQuery as
|
|
3231
|
+
import { useQuery as useQuery27 } from "@tanstack/react-query";
|
|
1344
3232
|
function usePages(locale, options) {
|
|
1345
3233
|
const { client } = useBehio();
|
|
1346
|
-
return
|
|
3234
|
+
return useQuery27({
|
|
1347
3235
|
queryKey: ["behio", "pages", locale],
|
|
1348
3236
|
queryFn: async () => {
|
|
1349
3237
|
const result = await unwrap(client.pages.list(locale));
|
|
@@ -1354,7 +3242,7 @@ function usePages(locale, options) {
|
|
|
1354
3242
|
}
|
|
1355
3243
|
function usePage(slug, locale, options) {
|
|
1356
3244
|
const { client } = useBehio();
|
|
1357
|
-
return
|
|
3245
|
+
return useQuery27({
|
|
1358
3246
|
queryKey: ["behio", "page", slug, locale],
|
|
1359
3247
|
queryFn: () => unwrap(client.pages.get(slug, locale)),
|
|
1360
3248
|
enabled: options?.enabled !== false && !!slug
|
|
@@ -1362,10 +3250,10 @@ function usePage(slug, locale, options) {
|
|
|
1362
3250
|
}
|
|
1363
3251
|
|
|
1364
3252
|
// src/react/hooks/use-shop-info.ts
|
|
1365
|
-
import { useQuery as
|
|
3253
|
+
import { useQuery as useQuery28 } from "@tanstack/react-query";
|
|
1366
3254
|
function useShopInfo(options) {
|
|
1367
3255
|
const { client } = useBehio();
|
|
1368
|
-
return
|
|
3256
|
+
return useQuery28({
|
|
1369
3257
|
queryKey: ["behio", "shop-info"],
|
|
1370
3258
|
queryFn: () => unwrap(client.getShopInfo()),
|
|
1371
3259
|
enabled: options?.enabled !== false
|
|
@@ -1373,10 +3261,10 @@ function useShopInfo(options) {
|
|
|
1373
3261
|
}
|
|
1374
3262
|
|
|
1375
3263
|
// src/react/hooks/use-shop-scripts.ts
|
|
1376
|
-
import { useQuery as
|
|
3264
|
+
import { useQuery as useQuery29 } from "@tanstack/react-query";
|
|
1377
3265
|
function useShopScripts(options) {
|
|
1378
3266
|
const { client } = useBehio();
|
|
1379
|
-
return
|
|
3267
|
+
return useQuery29({
|
|
1380
3268
|
queryKey: ["behio", "shop-scripts"],
|
|
1381
3269
|
queryFn: () => unwrap(client.getShopScripts()),
|
|
1382
3270
|
enabled: options?.enabled !== false
|
|
@@ -1384,11 +3272,11 @@ function useShopScripts(options) {
|
|
|
1384
3272
|
}
|
|
1385
3273
|
|
|
1386
3274
|
// src/react/hooks/use-shop-seo.ts
|
|
1387
|
-
import { useQuery as
|
|
3275
|
+
import { useQuery as useQuery30 } from "@tanstack/react-query";
|
|
1388
3276
|
function useShopSeo(options) {
|
|
1389
3277
|
const { client } = useBehio();
|
|
1390
3278
|
const { locale, initialData, enabled = true } = options ?? {};
|
|
1391
|
-
return
|
|
3279
|
+
return useQuery30({
|
|
1392
3280
|
queryKey: ["behio", "shop-seo", locale ?? "_default"],
|
|
1393
3281
|
queryFn: () => unwrap(client.getShopSeo(locale)),
|
|
1394
3282
|
initialData,
|
|
@@ -1451,20 +3339,20 @@ function CurrencySwitcher({
|
|
|
1451
3339
|
import { useEffect as useEffect5, useMemo as useMemo4, useState as useState9 } from "react";
|
|
1452
3340
|
|
|
1453
3341
|
// src/react/hooks/use-consent.ts
|
|
1454
|
-
import { useQuery as
|
|
3342
|
+
import { useQuery as useQuery31, useMutation as useMutation15, useQueryClient as useQueryClient15 } from "@tanstack/react-query";
|
|
1455
3343
|
function useCookieConsent(visitorId) {
|
|
1456
3344
|
const { client } = useBehio();
|
|
1457
|
-
const qc =
|
|
1458
|
-
const query =
|
|
3345
|
+
const qc = useQueryClient15();
|
|
3346
|
+
const query = useQuery31({
|
|
1459
3347
|
queryKey: ["behio", "consent", visitorId],
|
|
1460
3348
|
queryFn: () => unwrap(client.consent.get(visitorId)),
|
|
1461
3349
|
enabled: Boolean(visitorId)
|
|
1462
3350
|
});
|
|
1463
|
-
const recordMutation =
|
|
3351
|
+
const recordMutation = useMutation15({
|
|
1464
3352
|
mutationFn: (input) => unwrap(client.consent.record(input)),
|
|
1465
3353
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["behio", "consent"] })
|
|
1466
3354
|
});
|
|
1467
|
-
const revokeMutation =
|
|
3355
|
+
const revokeMutation = useMutation15({
|
|
1468
3356
|
mutationFn: () => unwrap(client.consent.revoke(visitorId)),
|
|
1469
3357
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["behio", "consent"] })
|
|
1470
3358
|
});
|
|
@@ -1823,10 +3711,10 @@ function utmFromSearch(search) {
|
|
|
1823
3711
|
}
|
|
1824
3712
|
|
|
1825
3713
|
// src/react/hooks/use-bundles.ts
|
|
1826
|
-
import { useQuery as
|
|
3714
|
+
import { useQuery as useQuery32 } from "@tanstack/react-query";
|
|
1827
3715
|
function useBundles(options) {
|
|
1828
3716
|
const { client } = useBehio();
|
|
1829
|
-
return
|
|
3717
|
+
return useQuery32({
|
|
1830
3718
|
queryKey: ["behio", "bundles"],
|
|
1831
3719
|
queryFn: () => unwrap(client.catalog.getBundles()),
|
|
1832
3720
|
enabled: options?.enabled ?? true,
|
|
@@ -1835,7 +3723,7 @@ function useBundles(options) {
|
|
|
1835
3723
|
}
|
|
1836
3724
|
function useBundle(slug, options) {
|
|
1837
3725
|
const { client } = useBehio();
|
|
1838
|
-
return
|
|
3726
|
+
return useQuery32({
|
|
1839
3727
|
queryKey: ["behio", "bundle", slug],
|
|
1840
3728
|
queryFn: () => unwrap(client.catalog.getBundle(slug)),
|
|
1841
3729
|
enabled: Boolean(slug) && (options?.enabled ?? true),
|
|
@@ -1844,10 +3732,10 @@ function useBundle(slug, options) {
|
|
|
1844
3732
|
}
|
|
1845
3733
|
|
|
1846
3734
|
// src/react/hooks/use-product-group.ts
|
|
1847
|
-
import { useQuery as
|
|
3735
|
+
import { useQuery as useQuery33 } from "@tanstack/react-query";
|
|
1848
3736
|
function useProductGroup(slug, options) {
|
|
1849
3737
|
const { client } = useBehio();
|
|
1850
|
-
return
|
|
3738
|
+
return useQuery33({
|
|
1851
3739
|
queryKey: ["behio", "product-group", slug, options?.locale, options?.currency],
|
|
1852
3740
|
queryFn: () => unwrap(
|
|
1853
3741
|
client.catalog.getProductGroup(slug, {
|
|
@@ -1861,10 +3749,10 @@ function useProductGroup(slug, options) {
|
|
|
1861
3749
|
}
|
|
1862
3750
|
|
|
1863
3751
|
// src/react/hooks/use-cross-sell.ts
|
|
1864
|
-
import { useQuery as
|
|
3752
|
+
import { useQuery as useQuery34 } from "@tanstack/react-query";
|
|
1865
3753
|
function useCrossSell(productSlug, options) {
|
|
1866
3754
|
const { client } = useBehio();
|
|
1867
|
-
return
|
|
3755
|
+
return useQuery34({
|
|
1868
3756
|
queryKey: ["behio", "cross-sell", productSlug, options?.locale, options?.currency],
|
|
1869
3757
|
queryFn: () => unwrap(
|
|
1870
3758
|
client.catalog.getCrossSell(productSlug, {
|
|
@@ -1878,10 +3766,10 @@ function useCrossSell(productSlug, options) {
|
|
|
1878
3766
|
}
|
|
1879
3767
|
|
|
1880
3768
|
// src/react/hooks/use-product-promotions.ts
|
|
1881
|
-
import { useQuery as
|
|
3769
|
+
import { useQuery as useQuery35 } from "@tanstack/react-query";
|
|
1882
3770
|
function useProductPromotions(productSlug, options) {
|
|
1883
3771
|
const { client } = useBehio();
|
|
1884
|
-
return
|
|
3772
|
+
return useQuery35({
|
|
1885
3773
|
queryKey: ["behio", "product-promotions", productSlug],
|
|
1886
3774
|
queryFn: () => unwrap(client.catalog.getProductPromotions(productSlug)),
|
|
1887
3775
|
enabled: Boolean(productSlug) && (options?.enabled ?? true),
|
|
@@ -1890,11 +3778,11 @@ function useProductPromotions(productSlug, options) {
|
|
|
1890
3778
|
}
|
|
1891
3779
|
|
|
1892
3780
|
// src/react/hooks/use-gift-card.ts
|
|
1893
|
-
import { useQuery as
|
|
3781
|
+
import { useQuery as useQuery36 } from "@tanstack/react-query";
|
|
1894
3782
|
function useGiftCardBalance(code, options) {
|
|
1895
3783
|
const { client } = useBehio();
|
|
1896
3784
|
const trimmed = code?.trim();
|
|
1897
|
-
return
|
|
3785
|
+
return useQuery36({
|
|
1898
3786
|
queryKey: ["behio", "gift-card-balance", trimmed],
|
|
1899
3787
|
queryFn: () => unwrap(client.catalog.checkGiftCard(trimmed)),
|
|
1900
3788
|
enabled: Boolean(trimmed && trimmed.length >= 6) && (options?.enabled ?? true)
|
|
@@ -1902,20 +3790,20 @@ function useGiftCardBalance(code, options) {
|
|
|
1902
3790
|
}
|
|
1903
3791
|
|
|
1904
3792
|
// src/react/hooks/use-wishlist.ts
|
|
1905
|
-
import { useQuery as
|
|
3793
|
+
import { useQuery as useQuery37, useMutation as useMutation16, useQueryClient as useQueryClient16 } from "@tanstack/react-query";
|
|
1906
3794
|
function useWishlist(options) {
|
|
1907
3795
|
const { client } = useBehio();
|
|
1908
|
-
const qc =
|
|
1909
|
-
const query =
|
|
3796
|
+
const qc = useQueryClient16();
|
|
3797
|
+
const query = useQuery37({
|
|
1910
3798
|
queryKey: ["behio", "wishlist"],
|
|
1911
3799
|
queryFn: () => unwrap(client.wishlist.get()),
|
|
1912
3800
|
enabled: options?.enabled ?? true
|
|
1913
3801
|
});
|
|
1914
|
-
const addMutation =
|
|
3802
|
+
const addMutation = useMutation16({
|
|
1915
3803
|
mutationFn: (productId) => unwrap(client.wishlist.add(productId)),
|
|
1916
3804
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["behio", "wishlist"] })
|
|
1917
3805
|
});
|
|
1918
|
-
const removeMutation =
|
|
3806
|
+
const removeMutation = useMutation16({
|
|
1919
3807
|
mutationFn: (productId) => unwrap(client.wishlist.remove(productId)),
|
|
1920
3808
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["behio", "wishlist"] })
|
|
1921
3809
|
});
|
|
@@ -1929,7 +3817,7 @@ function useWishlist(options) {
|
|
|
1929
3817
|
}
|
|
1930
3818
|
function useIsInWishlist(productId) {
|
|
1931
3819
|
const { client } = useBehio();
|
|
1932
|
-
return
|
|
3820
|
+
return useQuery37({
|
|
1933
3821
|
queryKey: ["behio", "wishlist-check", productId],
|
|
1934
3822
|
queryFn: () => unwrap(client.wishlist.isInWishlist(productId)),
|
|
1935
3823
|
enabled: Boolean(productId)
|
|
@@ -1937,10 +3825,10 @@ function useIsInWishlist(productId) {
|
|
|
1937
3825
|
}
|
|
1938
3826
|
|
|
1939
3827
|
// src/react/hooks/use-reviews.ts
|
|
1940
|
-
import { useQuery as
|
|
3828
|
+
import { useQuery as useQuery38, useMutation as useMutation17, useQueryClient as useQueryClient17 } from "@tanstack/react-query";
|
|
1941
3829
|
function useProductReviews(productId, options) {
|
|
1942
3830
|
const { client } = useBehio();
|
|
1943
|
-
return
|
|
3831
|
+
return useQuery38({
|
|
1944
3832
|
queryKey: ["behio", "reviews", productId, options?.page ?? 1],
|
|
1945
3833
|
queryFn: () => unwrap(client.reviews.getProductReviews(productId, options?.page, options?.limit)),
|
|
1946
3834
|
enabled: Boolean(productId) && (options?.enabled ?? true)
|
|
@@ -1948,30 +3836,30 @@ function useProductReviews(productId, options) {
|
|
|
1948
3836
|
}
|
|
1949
3837
|
function useSubmitReview() {
|
|
1950
3838
|
const { client } = useBehio();
|
|
1951
|
-
const qc =
|
|
1952
|
-
return
|
|
3839
|
+
const qc = useQueryClient17();
|
|
3840
|
+
return useMutation17({
|
|
1953
3841
|
mutationFn: (input) => unwrap(client.reviews.submit(input)),
|
|
1954
3842
|
onSuccess: (_, input) => qc.invalidateQueries({ queryKey: ["behio", "reviews", input.productId] })
|
|
1955
3843
|
});
|
|
1956
3844
|
}
|
|
1957
3845
|
|
|
1958
3846
|
// src/react/hooks/use-returns.ts
|
|
1959
|
-
import { useQuery as
|
|
3847
|
+
import { useQuery as useQuery39, useMutation as useMutation18 } from "@tanstack/react-query";
|
|
1960
3848
|
function useLookupReturnableOrder() {
|
|
1961
3849
|
const { client } = useBehio();
|
|
1962
|
-
return
|
|
3850
|
+
return useMutation18({
|
|
1963
3851
|
mutationFn: ({ orderNumber, email }) => unwrap(client.returns.lookupOrder(orderNumber, email))
|
|
1964
3852
|
});
|
|
1965
3853
|
}
|
|
1966
3854
|
function useSubmitReturn() {
|
|
1967
3855
|
const { client } = useBehio();
|
|
1968
|
-
return
|
|
3856
|
+
return useMutation18({
|
|
1969
3857
|
mutationFn: (input) => unwrap(client.returns.submit(input))
|
|
1970
3858
|
});
|
|
1971
3859
|
}
|
|
1972
3860
|
function useReturnStatus(returnId, email) {
|
|
1973
3861
|
const { client } = useBehio();
|
|
1974
|
-
return
|
|
3862
|
+
return useQuery39({
|
|
1975
3863
|
queryKey: ["behio", "return-status", returnId],
|
|
1976
3864
|
queryFn: () => unwrap(client.returns.getStatus(returnId, email)),
|
|
1977
3865
|
enabled: Boolean(returnId && email)
|
|
@@ -1979,16 +3867,16 @@ function useReturnStatus(returnId, email) {
|
|
|
1979
3867
|
}
|
|
1980
3868
|
|
|
1981
3869
|
// src/react/hooks/use-quotes.ts
|
|
1982
|
-
import { useMutation as
|
|
3870
|
+
import { useMutation as useMutation19, useQuery as useQuery40 } from "@tanstack/react-query";
|
|
1983
3871
|
function useSubmitQuote() {
|
|
1984
3872
|
const { client } = useBehio();
|
|
1985
|
-
return
|
|
3873
|
+
return useMutation19({
|
|
1986
3874
|
mutationFn: (input) => unwrap(client.quotes.submit(input))
|
|
1987
3875
|
});
|
|
1988
3876
|
}
|
|
1989
3877
|
function useQuoteStatus(quoteId, email) {
|
|
1990
3878
|
const { client } = useBehio();
|
|
1991
|
-
return
|
|
3879
|
+
return useQuery40({
|
|
1992
3880
|
queryKey: ["behio", "quote-status", quoteId],
|
|
1993
3881
|
queryFn: () => unwrap(client.quotes.getStatus(quoteId, email)),
|
|
1994
3882
|
enabled: Boolean(quoteId && email)
|
|
@@ -1996,13 +3884,123 @@ function useQuoteStatus(quoteId, email) {
|
|
|
1996
3884
|
}
|
|
1997
3885
|
|
|
1998
3886
|
// src/react/hooks/use-back-in-stock.ts
|
|
1999
|
-
import { useMutation as
|
|
3887
|
+
import { useMutation as useMutation20 } from "@tanstack/react-query";
|
|
2000
3888
|
function useNotifyWhenAvailable() {
|
|
2001
3889
|
const { client } = useBehio();
|
|
2002
|
-
return
|
|
3890
|
+
return useMutation20({
|
|
2003
3891
|
mutationFn: ({ productId, email }) => unwrap(client.catalog.notifyWhenAvailable(productId, email))
|
|
2004
3892
|
});
|
|
2005
3893
|
}
|
|
3894
|
+
|
|
3895
|
+
// src/react/utils/format-price.ts
|
|
3896
|
+
function formatPrice(amount, currency, locale) {
|
|
3897
|
+
const resolvedLocale = locale ?? "cs";
|
|
3898
|
+
try {
|
|
3899
|
+
return new Intl.NumberFormat(resolvedLocale, {
|
|
3900
|
+
style: "currency",
|
|
3901
|
+
currency,
|
|
3902
|
+
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2,
|
|
3903
|
+
maximumFractionDigits: 2
|
|
3904
|
+
}).format(amount);
|
|
3905
|
+
} catch {
|
|
3906
|
+
return `${amount} ${currency}`;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
// src/analytics.ts
|
|
3911
|
+
var GA4_NAME_MAP = {
|
|
3912
|
+
newsletter_signup: "generate_lead"
|
|
3913
|
+
};
|
|
3914
|
+
function trackEcommerceEvent(event, payload) {
|
|
3915
|
+
if (typeof window === "undefined") return;
|
|
3916
|
+
const w = window;
|
|
3917
|
+
try {
|
|
3918
|
+
w.__behioEcommerceSink?.(event, payload);
|
|
3919
|
+
} catch {
|
|
3920
|
+
}
|
|
3921
|
+
const gaName = GA4_NAME_MAP[event] ?? event;
|
|
3922
|
+
try {
|
|
3923
|
+
if (typeof w.gtag === "function") {
|
|
3924
|
+
w.gtag("event", gaName, payload);
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
if (Array.isArray(w.dataLayer)) {
|
|
3928
|
+
w.dataLayer.push({ ecommerce: null });
|
|
3929
|
+
w.dataLayer.push({ event: gaName, ecommerce: payload });
|
|
3930
|
+
}
|
|
3931
|
+
} catch {
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
// src/consent-visitor.ts
|
|
3936
|
+
var VISITOR_KEY3 = "behio_visitor_id";
|
|
3937
|
+
var COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
3938
|
+
function getStoredVisitorId() {
|
|
3939
|
+
if (typeof window === "undefined") return null;
|
|
3940
|
+
try {
|
|
3941
|
+
const ls = localStorage.getItem(VISITOR_KEY3);
|
|
3942
|
+
if (ls) return ls;
|
|
3943
|
+
} catch {
|
|
3944
|
+
}
|
|
3945
|
+
return readCookie(VISITOR_KEY3);
|
|
3946
|
+
}
|
|
3947
|
+
function generateVisitorId() {
|
|
3948
|
+
const bytes = new Uint8Array(18);
|
|
3949
|
+
try {
|
|
3950
|
+
globalThis.crypto?.getRandomValues?.(bytes);
|
|
3951
|
+
} catch {
|
|
3952
|
+
}
|
|
3953
|
+
let filled = false;
|
|
3954
|
+
for (const b of bytes) if (b !== 0) filled = true;
|
|
3955
|
+
if (!filled) for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
3956
|
+
let bin = "";
|
|
3957
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
3958
|
+
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bytes).toString("base64");
|
|
3959
|
+
return `v${b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`.slice(0, 40);
|
|
3960
|
+
}
|
|
3961
|
+
function writeVisitorId(id) {
|
|
3962
|
+
if (typeof window === "undefined") return;
|
|
3963
|
+
try {
|
|
3964
|
+
localStorage.setItem(VISITOR_KEY3, id);
|
|
3965
|
+
} catch {
|
|
3966
|
+
}
|
|
3967
|
+
try {
|
|
3968
|
+
document.cookie = `${VISITOR_KEY3}=${encodeURIComponent(id)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
3969
|
+
} catch {
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
function readCookie(name) {
|
|
3973
|
+
if (typeof document === "undefined") return null;
|
|
3974
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
|
3975
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
3976
|
+
}
|
|
3977
|
+
function emitConsentChanged() {
|
|
3978
|
+
if (typeof window === "undefined") return;
|
|
3979
|
+
try {
|
|
3980
|
+
window.dispatchEvent(new Event("behio:consent-changed"));
|
|
3981
|
+
} catch {
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
async function grantAnalyticsConsent(client, categories) {
|
|
3985
|
+
const id = getStoredVisitorId() ?? generateVisitorId();
|
|
3986
|
+
writeVisitorId(id);
|
|
3987
|
+
client.setAnalyticsVisitorId(id);
|
|
3988
|
+
const res = await client.consent.record({
|
|
3989
|
+
visitorId: id,
|
|
3990
|
+
analytics: true,
|
|
3991
|
+
marketing: categories?.marketing ?? false,
|
|
3992
|
+
preferences: categories?.preferences ?? false
|
|
3993
|
+
});
|
|
3994
|
+
emitConsentChanged();
|
|
3995
|
+
return res;
|
|
3996
|
+
}
|
|
3997
|
+
async function revokeAnalyticsConsent(client) {
|
|
3998
|
+
const id = getStoredVisitorId();
|
|
3999
|
+
client.setAnalyticsVisitorId(null);
|
|
4000
|
+
emitConsentChanged();
|
|
4001
|
+
if (!id) return { data: { success: true }, error: null };
|
|
4002
|
+
return client.consent.revoke(id);
|
|
4003
|
+
}
|
|
2006
4004
|
export {
|
|
2007
4005
|
BehioAnalyticsTracker,
|
|
2008
4006
|
BehioProvider,
|
|
@@ -2031,9 +4029,11 @@ export {
|
|
|
2031
4029
|
useCartCount,
|
|
2032
4030
|
useCategories,
|
|
2033
4031
|
useCategory,
|
|
4032
|
+
useCertificateVerification,
|
|
2034
4033
|
useCheckout,
|
|
2035
4034
|
useCookieConsent,
|
|
2036
4035
|
useCourse,
|
|
4036
|
+
useCourseCertificates,
|
|
2037
4037
|
useCourses,
|
|
2038
4038
|
useCrossSell,
|
|
2039
4039
|
useCurrency,
|
|
@@ -2044,6 +4044,10 @@ export {
|
|
|
2044
4044
|
useGiftCardBalance,
|
|
2045
4045
|
useIsInWishlist,
|
|
2046
4046
|
useLabels,
|
|
4047
|
+
useLessonComments,
|
|
4048
|
+
useLessonNote,
|
|
4049
|
+
useLessonQuiz,
|
|
4050
|
+
useLessonTutor,
|
|
2047
4051
|
useLookupReturnableOrder,
|
|
2048
4052
|
useLoyalty,
|
|
2049
4053
|
useMenu,
|