@behio/storefront-sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,7 +83,7 @@ function ProductList() {
83
83
  | `customer` | Profile, addresses, password change |
84
84
  | `wishlist` | Add, remove, check |
85
85
  | `reviews` | Submit, list, vote helpful |
86
- | `addresses` | Google Places autocomplete with debounce hook |
86
+ | `addresses` | Address autocomplete with debounce hook |
87
87
  | `returns` | Submit return requests |
88
88
  | `consent` | Cookie consent (GDPR) |
89
89
  | `quotes` | B2B quote requests |
@@ -72,6 +72,40 @@ var BehioNetworkError = class extends Error {
72
72
  this.code = isTimeout ? "TIMEOUT" : "NETWORK_ERROR";
73
73
  }
74
74
  };
75
+ function ok(data) {
76
+ return { data, error: null };
77
+ }
78
+ function err(error) {
79
+ return { data: null, error };
80
+ }
81
+ function toSdkError(err2) {
82
+ if (err2 instanceof BehioApiError) {
83
+ return {
84
+ code: err2.code,
85
+ message: err2.message,
86
+ status: err2.status,
87
+ body: err2.body,
88
+ isRetryable: err2.isRetryable,
89
+ cause: err2
90
+ };
91
+ }
92
+ if (err2 instanceof BehioNetworkError) {
93
+ return {
94
+ code: err2.code,
95
+ message: err2.message,
96
+ status: null,
97
+ isRetryable: err2.isRetryable,
98
+ cause: err2
99
+ };
100
+ }
101
+ return {
102
+ code: "UNKNOWN",
103
+ message: err2 instanceof Error ? err2.message : String(err2),
104
+ status: null,
105
+ isRetryable: false,
106
+ cause: err2
107
+ };
108
+ }
75
109
 
76
110
  // src/client.ts
77
111
  var BehioStorefront = class {
@@ -197,11 +231,17 @@ var BehioStorefront = class {
197
231
  this.isRefreshing = true;
198
232
  this.refreshPromise = (async () => {
199
233
  try {
200
- await this.auth.refresh();
234
+ const res = await this.auth.refresh();
235
+ if (res.error) {
236
+ this.clearTokens();
237
+ this.emit("auth:token-refresh-failed");
238
+ throw new BehioApiError(401, null, "Token refresh failed");
239
+ }
201
240
  this.emit("auth:token-refresh");
202
- } catch {
241
+ } catch (err2) {
203
242
  this.clearTokens();
204
243
  this.emit("auth:token-refresh-failed");
244
+ if (err2 instanceof BehioApiError) throw err2;
205
245
  throw new BehioApiError(401, null, "Token refresh failed");
206
246
  } finally {
207
247
  this.isRefreshing = false;
@@ -210,9 +250,29 @@ var BehioStorefront = class {
210
250
  })();
211
251
  return this.refreshPromise;
212
252
  }
213
- // --- Internal fetch ---
214
- /** @internal */
253
+ // --- Public request wrapper (SdkResult) ---
254
+ /**
255
+ * Every public module method funnels through here. Internally calls
256
+ * `rawRequest` (which throws on failure) and maps thrown errors to
257
+ * `SdkError` so the public surface can return `SdkResult<T>`.
258
+ *
259
+ * @internal — don't call from outside the SDK; use the typed module
260
+ * methods (behio.catalog.*, behio.cart.*, …) instead.
261
+ */
215
262
  async request(method, path, options) {
263
+ try {
264
+ const data = await this.rawRequest(method, path, options);
265
+ return ok(data);
266
+ } catch (err2) {
267
+ return { data: null, error: toSdkError(err2) };
268
+ }
269
+ }
270
+ /**
271
+ * Throws on failure (API error / network / timeout). Kept private so
272
+ * internal auth refresh recursion keeps its existing control flow —
273
+ * public callers must go through `request()` which returns Result.
274
+ */
275
+ async rawRequest(method, path, options) {
216
276
  const params = new URLSearchParams();
217
277
  if (options?.query) {
218
278
  for (const [key, value] of Object.entries(options.query)) {
@@ -275,11 +335,11 @@ var BehioStorefront = class {
275
335
  body: interceptedConfig.body,
276
336
  signal: controller.signal
277
337
  });
278
- } catch (err) {
338
+ } catch (err2) {
279
339
  clearTimeout(timeoutId);
280
- const isAbort = err instanceof DOMException && err.name === "AbortError";
340
+ const isAbort = err2 instanceof DOMException && err2.name === "AbortError";
281
341
  const networkErr = new BehioNetworkError(
282
- isAbort ? "Request timed out" : err.message || "Network error",
342
+ isAbort ? "Request timed out" : err2.message || "Network error",
283
343
  isAbort
284
344
  );
285
345
  this.emit("error", networkErr);
@@ -305,7 +365,7 @@ var BehioStorefront = class {
305
365
  if (res.status === 401 && this.refreshToken && options?.auth !== false && !options?._isRetryAfterRefresh) {
306
366
  try {
307
367
  await this.handleTokenRefresh();
308
- return this.request(method, path, { ...options, _isRetryAfterRefresh: true });
368
+ return this.rawRequest(method, path, { ...options, _isRetryAfterRefresh: true });
309
369
  } catch {
310
370
  this.emit("error", apiError);
311
371
  throw apiError;
@@ -371,7 +431,8 @@ var CatalogModule = class {
371
431
  /** Get category tree */
372
432
  async getCategories(locale) {
373
433
  const res = await this.client.request("GET", "/catalog/categories", { query: { locale } });
374
- return { categories: res.categories || res.items || [] };
434
+ if (res.error) return res;
435
+ return ok({ categories: res.data.categories || res.data.items || [] });
375
436
  }
376
437
  /** Get category detail by slug */
377
438
  async getCategory(slug, locale) {
@@ -392,7 +453,8 @@ var CatalogModule = class {
392
453
  /** Get all labels */
393
454
  async getLabels(locale) {
394
455
  const res = await this.client.request("GET", "/catalog/labels", { query: { locale } });
395
- return { labels: res.labels || res.items || [] };
456
+ if (res.error) return res;
457
+ return ok({ labels: res.data.labels || res.data.items || [] });
396
458
  }
397
459
  /** Get featured products */
398
460
  async getFeatured(options) {
@@ -418,7 +480,10 @@ var CatalogModule = class {
418
480
  }
419
481
  /** Cross-sell / related / upsell products for a product */
420
482
  async getCrossSell(productSlug) {
421
- return this.client.request("GET", `/catalog/products/${productSlug}/cross-sell`);
483
+ return this.client.request(
484
+ "GET",
485
+ `/catalog/products/${productSlug}/cross-sell`
486
+ );
422
487
  }
423
488
  /** Active promotions applicable to a product (with countdown end time) */
424
489
  async getProductPromotions(productSlug) {
@@ -435,44 +500,57 @@ var AuthModule = class {
435
500
  }
436
501
  /** Register a new customer */
437
502
  async register(input) {
438
- const tokens = await this.client.request("POST", "/auth/register", {
503
+ const res = await this.client.request("POST", "/auth/register", {
439
504
  body: input,
440
505
  auth: false
441
506
  });
442
- this.client.setTokens(tokens);
507
+ if (res.error) return res;
508
+ this.client.setTokens(res.data);
443
509
  this.client.emit("auth:login", { email: input.email });
444
- return tokens;
510
+ return res;
445
511
  }
446
512
  /** Login with email and password */
447
513
  async login(input) {
448
- const tokens = await this.client.request("POST", "/auth/login", {
514
+ const res = await this.client.request("POST", "/auth/login", {
449
515
  body: input,
450
516
  auth: false
451
517
  });
452
- this.client.setTokens(tokens);
518
+ if (res.error) return res;
519
+ this.client.setTokens(res.data);
453
520
  this.client.emit("auth:login", { email: input.email });
454
- return tokens;
521
+ return res;
455
522
  }
456
523
  /** Refresh access token using refresh token */
457
524
  async refresh(refreshToken) {
458
525
  const token = refreshToken || this.client.getRefreshToken();
459
- if (!token) throw new Error("No refresh token available");
460
- const tokens = await this.client.request("POST", "/auth/refresh", {
526
+ if (!token) {
527
+ return {
528
+ data: null,
529
+ error: {
530
+ code: "UNAUTHORIZED",
531
+ message: "No refresh token available",
532
+ status: null,
533
+ isRetryable: false
534
+ }
535
+ };
536
+ }
537
+ const res = await this.client.request("POST", "/auth/refresh", {
461
538
  body: { refreshToken: token },
462
539
  auth: false
463
540
  });
464
- this.client.setTokens(tokens);
465
- return tokens;
541
+ if (res.error) return res;
542
+ this.client.setTokens(res.data);
543
+ return res;
466
544
  }
467
545
  /** Logout (invalidate refresh token) */
468
546
  async logout(refreshToken) {
469
547
  const token = refreshToken || this.client.getRefreshToken();
470
- const result = await this.client.request("POST", "/auth/logout", {
548
+ const res = await this.client.request("POST", "/auth/logout", {
471
549
  body: { refreshToken: token }
472
550
  });
473
551
  this.client.clearTokens();
474
552
  this.client.emit("auth:logout");
475
- return result;
553
+ return res;
476
554
  }
477
555
  /** Request password reset email */
478
556
  async forgotPassword(email) {
@@ -510,90 +588,102 @@ var CartModule = class {
510
588
  }
511
589
  /** Add item to cart */
512
590
  async addItem(input) {
513
- const result = await this.client.request("POST", "/cart/items", {
591
+ const res = await this.client.request("POST", "/cart/items", {
514
592
  body: { whItemId: input.productId, quantity: input.quantity }
515
593
  });
516
- if (result.newSessionToken) {
517
- this.client.setCartSession(result.newSessionToken);
594
+ if (res.error) return res;
595
+ if (res.data.newSessionToken) {
596
+ this.client.setCartSession(res.data.newSessionToken);
518
597
  }
519
- this.client.emit("cart:updated", result);
520
- return result;
598
+ this.client.emit("cart:updated", res.data);
599
+ return res;
521
600
  }
522
601
  /** Update item quantity */
523
602
  async updateQuantity(itemId, quantity) {
524
- const result = await this.client.request("PATCH", `/cart/items/${itemId}`, {
603
+ const res = await this.client.request("PATCH", `/cart/items/${itemId}`, {
525
604
  body: { quantity }
526
605
  });
527
- this.client.emit("cart:updated", result);
528
- return result;
606
+ if (res.error) return res;
607
+ this.client.emit("cart:updated", res.data);
608
+ return res;
529
609
  }
530
610
  /** Remove item from cart */
531
611
  async removeItem(itemId) {
532
- const result = await this.client.request("DELETE", `/cart/items/${itemId}`);
533
- this.client.emit("cart:updated", result);
534
- return result;
612
+ const res = await this.client.request("DELETE", `/cart/items/${itemId}`);
613
+ if (res.error) return res;
614
+ this.client.emit("cart:updated", res.data);
615
+ return res;
535
616
  }
536
617
  /** Clear entire cart */
537
618
  async clear() {
538
- const result = await this.client.request("DELETE", "/cart");
619
+ const res = await this.client.request("DELETE", "/cart");
620
+ if (res.error) return res;
539
621
  this.client.emit("cart:cleared");
540
- return result;
622
+ return res;
541
623
  }
542
624
  /** Apply a gift card code to the cart. Balance is deducted at checkout. */
543
625
  async applyGiftCard(code) {
544
- const result = await this.client.request("POST", "/cart/gift-card", {
626
+ const res = await this.client.request("POST", "/cart/gift-card", {
545
627
  body: { code }
546
628
  });
547
- this.client.emit("cart:updated", result);
548
- return result;
629
+ if (res.error) return res;
630
+ this.client.emit("cart:updated", res.data);
631
+ return res;
549
632
  }
550
633
  /** Remove a gift card from the cart */
551
634
  async removeGiftCard() {
552
- const result = await this.client.request("DELETE", "/cart/gift-card");
553
- this.client.emit("cart:updated", result);
554
- return result;
635
+ const res = await this.client.request("DELETE", "/cart/gift-card");
636
+ if (res.error) return res;
637
+ this.client.emit("cart:updated", res.data);
638
+ return res;
555
639
  }
556
640
  /** Add a bundle to the cart (price is locked at the bundle's current price) */
557
641
  async addBundle(bundleId, quantity = 1) {
558
- const result = await this.client.request("POST", "/cart/bundles", {
642
+ const res = await this.client.request("POST", "/cart/bundles", {
559
643
  body: { bundleId, quantity }
560
644
  });
561
- this.client.emit("cart:updated", result);
562
- return result;
645
+ if (res.error) return res;
646
+ this.client.emit("cart:updated", res.data);
647
+ return res;
563
648
  }
564
649
  /** Update quantity of a bundle already in the cart */
565
650
  async updateBundleQuantity(bundleId, quantity) {
566
- const result = await this.client.request("PATCH", `/cart/bundles/${bundleId}`, {
651
+ const res = await this.client.request("PATCH", `/cart/bundles/${bundleId}`, {
567
652
  body: { quantity }
568
653
  });
569
- this.client.emit("cart:updated", result);
570
- return result;
654
+ if (res.error) return res;
655
+ this.client.emit("cart:updated", res.data);
656
+ return res;
571
657
  }
572
658
  /** Remove a bundle from the cart */
573
659
  async removeBundle(bundleId) {
574
- const result = await this.client.request("DELETE", `/cart/bundles/${bundleId}`);
575
- this.client.emit("cart:updated", result);
576
- return result;
660
+ const res = await this.client.request("DELETE", `/cart/bundles/${bundleId}`);
661
+ if (res.error) return res;
662
+ this.client.emit("cart:updated", res.data);
663
+ return res;
577
664
  }
578
665
  /** Merge anonymous cart into authenticated customer cart */
579
666
  async merge() {
580
- const result = await this.client.request("POST", "/cart/merge");
581
- this.client.emit("cart:updated", result);
582
- return result;
667
+ const res = await this.client.request("POST", "/cart/merge");
668
+ if (res.error) return res;
669
+ this.client.emit("cart:updated", res.data);
670
+ return res;
583
671
  }
584
672
  /** Apply discount code */
585
673
  async applyDiscount(code) {
586
- const result = await this.client.request("POST", "/cart/discount", {
674
+ const res = await this.client.request("POST", "/cart/discount", {
587
675
  body: { code }
588
676
  });
589
- this.client.emit("cart:updated", result);
590
- return result;
677
+ if (res.error) return res;
678
+ this.client.emit("cart:updated", res.data);
679
+ return res;
591
680
  }
592
681
  /** Remove discount code */
593
682
  async removeDiscount() {
594
- const result = await this.client.request("DELETE", "/cart/discount");
595
- this.client.emit("cart:updated", result);
596
- return result;
683
+ const res = await this.client.request("DELETE", "/cart/discount");
684
+ if (res.error) return res;
685
+ this.client.emit("cart:updated", res.data);
686
+ return res;
597
687
  }
598
688
  };
599
689
  var CheckoutModule = class {
@@ -602,13 +692,14 @@ var CheckoutModule = class {
602
692
  }
603
693
  /** Create order from cart */
604
694
  async createOrder(input) {
605
- const result = await this.client.request("POST", "/checkout", {
695
+ const res = await this.client.request("POST", "/checkout", {
606
696
  body: input
607
697
  });
698
+ if (res.error) return res;
608
699
  this.client.clearCartSession();
609
- this.client.emit("order:created", result);
700
+ this.client.emit("order:created", res.data);
610
701
  this.client.emit("cart:cleared");
611
- return result;
702
+ return res;
612
703
  }
613
704
  };
614
705
  var OrdersModule = class {
@@ -760,7 +851,7 @@ var AddressModule = class {
760
851
  }
761
852
  /** Search for address suggestions (debounce on your side, or use the React hook) */
762
853
  async autocomplete(query, country) {
763
- if (!query || query.length < 2) return { suggestions: [] };
854
+ if (!query || query.length < 2) return ok({ suggestions: [] });
764
855
  return this.client.request("GET", "/addresses/autocomplete", {
765
856
  query: { q: query, country }
766
857
  });
@@ -781,5 +872,8 @@ export {
781
872
  AddressTypes,
782
873
  BehioApiError,
783
874
  BehioNetworkError,
875
+ ok,
876
+ err,
877
+ toSdkError,
784
878
  BehioStorefront
785
879
  };