@behio/storefront-sdk 1.8.0 → 1.17.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
@@ -2,11 +2,11 @@
2
2
 
3
3
  **Headless e-commerce SDK for building custom storefronts.**
4
4
 
5
- Behio gives you a complete e-commerce backend products, inventory, orders, customers, discounts, multi-currency, multi-language and lets you design the storefront however you want. No themes, no templates, no vendor lock-in.
5
+ Behio gives you a complete e-commerce backend (products, inventory, orders, customers, discounts, multi-currency, multi-language) and lets you design the storefront however you want. Merchants run on it worldwide, with local tax rules, carriers and payment gateways handled natively. No themes, no templates, no vendor lock-in.
6
6
 
7
7
  ## Why Behio?
8
8
 
9
- - **You own the frontend.** Next.js, React, Vue, Nuxt, Astro, or plain JS the backend doesn't care.
9
+ - **You own the frontend.** Next.js, React, Vue, Nuxt, Astro, or plain JS, the backend doesn't care.
10
10
  - **Production-ready in minutes.** Catalog, cart, checkout, customer accounts, orders, CMS, discount codes, gift cards, loyalty programs, and more.
11
11
  - **Built for developers.** Full TypeScript types, auto-completing, modern React hooks with TanStack Query.
12
12
  - **Scale-ready.** Redis caching, rate limiting, webhooks, atomic checkout (no double-spend, no overselling).
@@ -60,7 +60,7 @@ function ProductList() {
60
60
 
61
61
  return data.items.map(p => (
62
62
  <div key={p.id}>
63
- <h3>{p.name} {p.price} {p.currency}</h3>
63
+ <h3>{p.name}, {p.price} {p.currency}</h3>
64
64
  <button onClick={() => addToCart.mutateAsync({ productId: p.id, quantity: 1 })}>
65
65
  Add to Cart
66
66
  </button>
@@ -89,18 +89,20 @@ function ProductList() {
89
89
  | `consent` | Cookie consent (GDPR) |
90
90
  | `quotes` | B2B quote requests |
91
91
  | `pages` | CMS pages |
92
+ | `blog` | Blogs and published posts (web + e-shop) |
93
+ | `forms` | Merchant-defined forms: definition + validated submit with per-field errors (web + e-shop) |
92
94
 
93
95
  ### React Hooks (30+)
94
96
 
95
- `useProducts` · `useProduct` · `useCategories` · `useCategoryProducts` · `useFeaturedProducts` · `useLabels` · `useProductSearch` · `useFilters` · `useBundles` · `useBundle` · `useCrossSell` · `useProductPromotions` · `useGiftCardBalance` · `useCart` · `useAddToCart` · `useUpdateCartItem` · `useRemoveCartItem` · `useCheckout` · `useOrders` · `useOrder` · `useOrderTracking` · `useCustomerProfile` · `useAddresses` · `useAddressAutocomplete` · `useWishlist` · `useProductReviews` · `useSubmitReview` · `useShopInfo` · `useShopSeo` · `useCartCount`
97
+ `useProducts` · `useProduct` · `useCategories` · `useCategoryProducts` · `useFeaturedProducts` · `useLabels` · `useProductSearch` · `useFilters` · `useBundles` · `useBundle` · `useCrossSell` · `useProductPromotions` · `useGiftCardBalance` · `useCart` · `useAddToCart` · `useUpdateCartItem` · `useRemoveCartItem` · `useCheckout` · `useOrders` · `useOrder` · `useOrderTracking` · `useCustomerProfile` · `useAddresses` · `useAddressAutocomplete` · `useWishlist` · `useProductReviews` · `useSubmitReview` · `useShopInfo` · `useShopSeo` · `useCartCount` · `useBlogs` · `useBlogPosts` · `useBlogPost` · `useSiteForm` · `useSiteFormSubmit`
96
98
 
97
99
  ### Framework Support
98
100
 
99
- - **Next.js** Server components + client hooks, SSR ready
100
- - **React + Vite** Standard SPA setup
101
- - **Nuxt 3** Composables with SSR
102
- - **Vue + Vite** Provide/inject pattern
103
- - **Vanilla JS** Works in any runtime (Node.js, Deno, Bun, Cloudflare Workers)
101
+ - **Next.js**: Server components + client hooks, SSR ready
102
+ - **React + Vite**: Standard SPA setup
103
+ - **Nuxt 3**: Composables with SSR
104
+ - **Vue + Vite**: Provide/inject pattern
105
+ - **Vanilla JS**: Works in any runtime (Node.js, Deno, Bun, Cloudflare Workers)
104
106
 
105
107
  ### Built-in Features
106
108
 
@@ -33,6 +33,38 @@ var AddressTypes = {
33
33
  SHIPPING: "SHIPPING",
34
34
  BILLING: "BILLING"
35
35
  };
36
+ var BE_CODE_TO_ERROR = {
37
+ "be.storefront.cartIsEmpty": "CART_EMPTY",
38
+ "be.storefront.productNotFound": "PRODUCT_NOT_FOUND",
39
+ "be.storefront.productNotAvailable": "PRODUCT_NOT_FOUND",
40
+ "be.storefront.invalidEmailOrPassword": "INVALID_CREDENTIALS",
41
+ "be.storefront.currentPasswordIncorrect": "INVALID_CREDENTIALS",
42
+ "be.storefront.discountExpired": "DISCOUNT_EXPIRED",
43
+ "be.storefront.discountExpiredOrLimit": "DISCOUNT_EXPIRED",
44
+ "be.storefront.invalidDiscountCode": "INVALID_DISCOUNT",
45
+ "be.storefront.discountInvalid": "INVALID_DISCOUNT",
46
+ "be.storefront.discountInactive": "INVALID_DISCOUNT",
47
+ "be.storefront.discountWrongCurrency": "INVALID_DISCOUNT",
48
+ "be.storefront.discountNotApplicable": "INVALID_DISCOUNT",
49
+ "be.storefront.discountNotYetValid": "INVALID_DISCOUNT",
50
+ "be.storefront.discountUsageLimitReached": "INVALID_DISCOUNT",
51
+ "be.storefront.discountLimitedToGroups": "INVALID_DISCOUNT",
52
+ "be.storefront.tokenInvalid": "TOKEN_INVALID",
53
+ "be.storefront.orderAccessTokenInvalid": "TOKEN_INVALID",
54
+ "be.storefront.resetTokenInvalid": "TOKEN_INVALID",
55
+ "be.storefront.invalidVerificationToken": "TOKEN_INVALID",
56
+ "be.storefront.invalidRefreshToken": "TOKEN_INVALID",
57
+ "be.storefront.refreshTokenExpired": "TOKEN_EXPIRED",
58
+ "be.storefront.apiKeyExpired": "TOKEN_EXPIRED",
59
+ "be.storefront.customerEmailTaken": "EMAIL_ALREADY_EXISTS",
60
+ "be.storefront.orderNotCancellable": "ORDER_NOT_CANCELLABLE",
61
+ "be.storefront.orderCancelledNotReturnable": "ORDER_NOT_CANCELLABLE",
62
+ "be.validation.failed": "VALIDATION_ERROR",
63
+ "be.forms.formNotFound": "NOT_FOUND",
64
+ "be.forms.validationFailed": "VALIDATION_ERROR",
65
+ "be.forms.consentRequired": "VALIDATION_ERROR",
66
+ "be.forms.tooLarge": "VALIDATION_ERROR"
67
+ };
36
68
  var BehioApiError = class _BehioApiError extends Error {
37
69
  constructor(status, body, message) {
38
70
  super(message || `API Error ${status}`);
@@ -49,7 +81,12 @@ var BehioApiError = class _BehioApiError extends Error {
49
81
  if (status === 409) return "EMAIL_ALREADY_EXISTS";
50
82
  if (status === 429) return "RATE_LIMITED";
51
83
  if (status >= 500) return "INTERNAL_ERROR";
52
- const msg = (_optionalChain([body, 'optionalAccess', _ => _.message]) || "").toLowerCase();
84
+ const rawCode = _optionalChain([body, 'optionalAccess', _ => _.code]);
85
+ if (typeof rawCode === "string") {
86
+ const mapped = BE_CODE_TO_ERROR[rawCode];
87
+ if (mapped) return mapped;
88
+ }
89
+ const msg = (_optionalChain([body, 'optionalAccess', _2 => _2.message]) || "").toLowerCase();
53
90
  if (msg.includes("invalid") && msg.includes("password"))
54
91
  return "INVALID_CREDENTIALS";
55
92
  if (msg.includes("invalid") && msg.includes("email"))
@@ -157,6 +194,8 @@ var BehioStorefront = class {
157
194
  this.orders = new OrdersModule(this);
158
195
  this.customer = new CustomerModule(this);
159
196
  this.pages = new PagesModule(this);
197
+ this.blog = new BlogModule(this);
198
+ this.forms = new FormsModule(this);
160
199
  this.wishlist = new WishlistModule(this);
161
200
  this.reviews = new ReviewsModule(this);
162
201
  this.returns = new ReturnsModule(this);
@@ -225,6 +264,21 @@ var BehioStorefront = class {
225
264
  body: input
226
265
  });
227
266
  }
267
+ /**
268
+ * Visitor messages from merchant automations (storefront.event action).
269
+ * Consent-gated visitor id; each message carries a merchant-defined `name`
270
+ * and free-form `payload` the template reacts to (modal, banner, ...).
271
+ * Messages stay listed until acknowledged via `ackVisitorMessage`.
272
+ */
273
+ async getVisitorMessages(visitorId) {
274
+ return this.request("GET", "/messages", { query: { visitorId } });
275
+ }
276
+ /** Acknowledge a visitor message so it is not delivered again. */
277
+ async ackVisitorMessage(messageId, visitorId) {
278
+ return this.request("POST", `/messages/${messageId}/ack`, {
279
+ query: { visitorId }
280
+ });
281
+ }
228
282
  /** Get basic shop info */
229
283
  async getShopInfo() {
230
284
  return this.request("GET", "/shop");
@@ -302,12 +356,12 @@ var BehioStorefront = class {
302
356
  if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
303
357
  this.listeners.get(event).add(handler);
304
358
  return () => {
305
- _optionalChain([this, 'access', _2 => _2.listeners, 'access', _3 => _3.get, 'call', _4 => _4(event), 'optionalAccess', _5 => _5.delete, 'call', _6 => _6(handler)]);
359
+ _optionalChain([this, 'access', _3 => _3.listeners, 'access', _4 => _4.get, 'call', _5 => _5(event), 'optionalAccess', _6 => _6.delete, 'call', _7 => _7(handler)]);
306
360
  };
307
361
  }
308
362
  /** @internal Emit an event (fire-and-forget, handler errors are swallowed) */
309
363
  emit(event, data) {
310
- _optionalChain([this, 'access', _7 => _7.listeners, 'access', _8 => _8.get, 'call', _9 => _9(event), 'optionalAccess', _10 => _10.forEach, 'call', _11 => _11((fn) => {
364
+ _optionalChain([this, 'access', _8 => _8.listeners, 'access', _9 => _9.get, 'call', _10 => _10(event), 'optionalAccess', _11 => _11.forEach, 'call', _12 => _12((fn) => {
311
365
  try {
312
366
  fn(data);
313
367
  } catch (e2) {
@@ -423,7 +477,7 @@ var BehioStorefront = class {
423
477
  */
424
478
  async rawRequest(method, path, options) {
425
479
  const params = new URLSearchParams();
426
- if (_optionalChain([options, 'optionalAccess', _12 => _12.query])) {
480
+ if (_optionalChain([options, 'optionalAccess', _13 => _13.query])) {
427
481
  for (const [key, value] of Object.entries(options.query)) {
428
482
  if (value === void 0 || value === null || value === "") continue;
429
483
  if (Array.isArray(value)) {
@@ -452,7 +506,7 @@ var BehioStorefront = class {
452
506
  if (this.shopDomain) {
453
507
  headers["X-Shop-Domain"] = this.shopDomain;
454
508
  }
455
- if (this.accessToken && _optionalChain([options, 'optionalAccess', _13 => _13.auth]) !== false) {
509
+ if (this.accessToken && _optionalChain([options, 'optionalAccess', _14 => _14.auth]) !== false) {
456
510
  headers["Authorization"] = `Bearer ${this.accessToken}`;
457
511
  }
458
512
  if (this.cartSession) {
@@ -468,10 +522,10 @@ var BehioStorefront = class {
468
522
  } catch (e4) {
469
523
  }
470
524
  }
471
- if (_optionalChain([options, 'optionalAccess', _14 => _14.headers])) {
525
+ if (_optionalChain([options, 'optionalAccess', _15 => _15.headers])) {
472
526
  Object.assign(headers, options.headers);
473
527
  }
474
- const bodyStr = _optionalChain([options, 'optionalAccess', _15 => _15.body]) ? JSON.stringify(options.body) : void 0;
528
+ const bodyStr = _optionalChain([options, 'optionalAccess', _16 => _16.body]) ? JSON.stringify(options.body) : void 0;
475
529
  let interceptedConfig = {
476
530
  url,
477
531
  method,
@@ -485,7 +539,7 @@ var BehioStorefront = class {
485
539
  for (let attempt = 0; attempt <= this.retries; attempt++) {
486
540
  const controller = new AbortController();
487
541
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
488
- if (_optionalChain([options, 'optionalAccess', _16 => _16.signal])) {
542
+ if (_optionalChain([options, 'optionalAccess', _17 => _17.signal])) {
489
543
  if (options.signal.aborted) {
490
544
  clearTimeout(timeoutId);
491
545
  throw new BehioNetworkError("Request aborted", false);
@@ -536,9 +590,9 @@ var BehioStorefront = class {
536
590
  const apiError = new BehioApiError(
537
591
  res.status,
538
592
  body,
539
- _optionalChain([body, 'optionalAccess', _17 => _17.message]) || `API Error ${res.status}`
593
+ _optionalChain([body, 'optionalAccess', _18 => _18.message]) || `API Error ${res.status}`
540
594
  );
541
- if (res.status === 401 && this.refreshToken && _optionalChain([options, 'optionalAccess', _18 => _18.auth]) !== false && !_optionalChain([options, 'optionalAccess', _19 => _19._isRetryAfterRefresh])) {
595
+ if (res.status === 401 && this.refreshToken && _optionalChain([options, 'optionalAccess', _19 => _19.auth]) !== false && !_optionalChain([options, 'optionalAccess', _20 => _20._isRetryAfterRefresh])) {
542
596
  try {
543
597
  await this.handleTokenRefresh();
544
598
  return this.rawRequest(method, path, {
@@ -622,7 +676,7 @@ var CatalogModule = class {
622
676
  "GET",
623
677
  `/catalog/products/${slug}`,
624
678
  {
625
- query: { locale: _optionalChain([options, 'optionalAccess', _20 => _20.locale]), currency: _optionalChain([options, 'optionalAccess', _21 => _21.currency]) }
679
+ query: { locale: _optionalChain([options, 'optionalAccess', _21 => _21.locale]), currency: _optionalChain([options, 'optionalAccess', _22 => _22.currency]) }
626
680
  }
627
681
  );
628
682
  }
@@ -676,7 +730,7 @@ var CatalogModule = class {
676
730
  "GET",
677
731
  `/catalog/menu/${encodeURIComponent(handle)}`,
678
732
  {
679
- query: { locale: _optionalChain([options, 'optionalAccess', _22 => _22.locale]) }
733
+ query: { locale: _optionalChain([options, 'optionalAccess', _23 => _23.locale]) }
680
734
  }
681
735
  );
682
736
  }
@@ -692,7 +746,7 @@ var CatalogModule = class {
692
746
  "GET",
693
747
  "/catalog/featured",
694
748
  {
695
- query: { locale: _optionalChain([options, 'optionalAccess', _23 => _23.locale]), currency: _optionalChain([options, 'optionalAccess', _24 => _24.currency]) }
749
+ query: { locale: _optionalChain([options, 'optionalAccess', _24 => _24.locale]), currency: _optionalChain([options, 'optionalAccess', _25 => _25.currency]) }
696
750
  }
697
751
  );
698
752
  }
@@ -707,7 +761,7 @@ var CatalogModule = class {
707
761
  return this.client.request(
708
762
  "GET",
709
763
  "/catalog/filters",
710
- { query: { locale: _optionalChain([options, 'optionalAccess', _25 => _25.locale]) } }
764
+ { query: { locale: _optionalChain([options, 'optionalAccess', _26 => _26.locale]) } }
711
765
  );
712
766
  }
713
767
  /**
@@ -722,7 +776,7 @@ var CatalogModule = class {
722
776
  return this.client.request(
723
777
  "GET",
724
778
  `/catalog/products/${slug}/parameters`,
725
- { query: { locale: _optionalChain([options, 'optionalAccess', _26 => _26.locale]) } }
779
+ { query: { locale: _optionalChain([options, 'optionalAccess', _27 => _27.locale]) } }
726
780
  );
727
781
  }
728
782
  /**
@@ -734,7 +788,7 @@ var CatalogModule = class {
734
788
  return this.client.request(
735
789
  "GET",
736
790
  `/catalog/products/${slug}/parameters/${groupSlug}`,
737
- { query: { locale: _optionalChain([options, 'optionalAccess', _27 => _27.locale]) } }
791
+ { query: { locale: _optionalChain([options, 'optionalAccess', _28 => _28.locale]) } }
738
792
  );
739
793
  }
740
794
  /**
@@ -792,7 +846,7 @@ var CatalogModule = class {
792
846
  "GET",
793
847
  `/catalog/product-groups/${encodeURIComponent(slug)}`,
794
848
  {
795
- query: { locale: _optionalChain([options, 'optionalAccess', _28 => _28.locale]), currency: _optionalChain([options, 'optionalAccess', _29 => _29.currency]) }
849
+ query: { locale: _optionalChain([options, 'optionalAccess', _29 => _29.locale]), currency: _optionalChain([options, 'optionalAccess', _30 => _30.currency]) }
796
850
  }
797
851
  );
798
852
  }
@@ -807,7 +861,7 @@ var CatalogModule = class {
807
861
  return this.client.request(
808
862
  "GET",
809
863
  `/catalog/products/${encodeURIComponent(productSlug)}/cross-sell`,
810
- { query: { locale: _optionalChain([options, 'optionalAccess', _30 => _30.locale]), currency: _optionalChain([options, 'optionalAccess', _31 => _31.currency]) } }
864
+ { query: { locale: _optionalChain([options, 'optionalAccess', _31 => _31.locale]), currency: _optionalChain([options, 'optionalAccess', _32 => _32.currency]) } }
811
865
  );
812
866
  }
813
867
  /** Active promotions applicable to a product (with countdown end time) */
@@ -844,16 +898,27 @@ var CatalogModule = class {
844
898
  { body: input }
845
899
  );
846
900
  }
847
- /** List configured payment methods (filtered by currency). */
901
+ /**
902
+ * List configured payment methods (filtered by currency). `locale` picks the
903
+ * language of `instruments[].label` / `swifts[].label` (SDK 1.14.0); pass the
904
+ * locale of the page so the instrument tiles read in the shopper's language.
905
+ */
848
906
  async listPaymentMethods(opts) {
849
907
  const query = {};
850
- if (_optionalChain([opts, 'optionalAccess', _32 => _32.currency])) query.currency = opts.currency;
908
+ if (_optionalChain([opts, 'optionalAccess', _33 => _33.currency])) query.currency = opts.currency;
909
+ if (_optionalChain([opts, 'optionalAccess', _34 => _34.locale])) query.locale = opts.locale;
910
+ if (_optionalChain([opts, 'optionalAccess', _35 => _35.country])) query.country = opts.country;
911
+ if (_optionalChain([opts, 'optionalAccess', _36 => _36.shippingMethodId])) query.shippingMethodId = opts.shippingMethodId;
851
912
  return this.client.request(
852
913
  "GET",
853
914
  "/catalog/payment-methods",
854
915
  { query }
855
916
  );
856
917
  }
918
+ /** Alias of `listPaymentMethods` (SDK 1.14.0). */
919
+ paymentMethods(opts) {
920
+ return this.listPaymentMethods(opts);
921
+ }
857
922
  };
858
923
  var AuthModule = class {
859
924
  constructor(client) {
@@ -1007,6 +1072,26 @@ var CartModule = class {
1007
1072
  this.client.emit("cart:updated", res.data);
1008
1073
  return res;
1009
1074
  }
1075
+ /**
1076
+ * Tell the cart where the order will ship (and, for B2B, the buyer's VAT
1077
+ * ID) so the VAT breakdown matches the checkout before the address form:
1078
+ * destination-country rate (OSS), 0 % export outside the EU, or reverse
1079
+ * charge for an EU business with a VIES-valid VAT ID. `cart.vatMode` says
1080
+ * which rule applied. SDK 1.17.0.
1081
+ *
1082
+ * ```ts
1083
+ * await client.cart.setDestination({ country: "SK" });
1084
+ * await client.cart.setDestination({ vatId: "SK2020000001" }); // "" clears
1085
+ * ```
1086
+ */
1087
+ async setDestination(input) {
1088
+ const res = await this.client.request("PUT", "/cart/destination", {
1089
+ body: input
1090
+ });
1091
+ if (res.error) return res;
1092
+ this.client.emit("cart:updated", res.data);
1093
+ return res;
1094
+ }
1010
1095
  /** Update item quantity */
1011
1096
  async updateQuantity(itemId, quantity) {
1012
1097
  const res = await this.client.request(
@@ -1158,13 +1243,37 @@ var OrdersModule = class {
1158
1243
  constructor(client) {
1159
1244
  this.client = client;
1160
1245
  }
1246
+ /**
1247
+ * Ask the backend to re-check this order's payment with the gateway.
1248
+ *
1249
+ * Call it on the thank-you page the customer lands on after paying, BEFORE
1250
+ * you read the order. Some gateways (Tatrapay+) have no server-to-server
1251
+ * notification at all, so the customer's return is the only fast way the
1252
+ * payment gets confirmed; for the others it is a safety net for a lost
1253
+ * notification.
1254
+ *
1255
+ * The response is deliberately opaque (`{ok: true}` every time, even for an
1256
+ * order number that does not exist): order numbers are sequential, so
1257
+ * anything else would turn this into a probe for other people's orders.
1258
+ * Read the actual state afterwards through a path that proves entitlement
1259
+ * ({@link get}, {@link track} or the guest access-code flow).
1260
+ *
1261
+ * Never throws for a missing order and never blocks the page: treat a
1262
+ * failure as "not confirmed yet", the backend poller catches up on its own.
1263
+ */
1264
+ async syncPaymentOnReturn(orderNumber) {
1265
+ return this.client.request(
1266
+ "POST",
1267
+ `/payments/return/${orderNumber}`
1268
+ );
1269
+ }
1161
1270
  /** List customer orders (requires auth) */
1162
1271
  async list(options) {
1163
1272
  return this.client.request(
1164
1273
  "GET",
1165
1274
  "/orders",
1166
1275
  {
1167
- query: { page: _optionalChain([options, 'optionalAccess', _33 => _33.page]), limit: _optionalChain([options, 'optionalAccess', _34 => _34.limit]) }
1276
+ query: { page: _optionalChain([options, 'optionalAccess', _37 => _37.page]), limit: _optionalChain([options, 'optionalAccess', _38 => _38.limit]) }
1168
1277
  }
1169
1278
  );
1170
1279
  }
@@ -1510,6 +1619,59 @@ var PagesModule = class {
1510
1619
  });
1511
1620
  }
1512
1621
  };
1622
+ var BlogModule = class {
1623
+ constructor(client) {
1624
+ this.client = client;
1625
+ }
1626
+ /** List the site's blogs (active only). */
1627
+ async list(locale) {
1628
+ return this.client.request("GET", "/blogs", {
1629
+ query: { locale }
1630
+ });
1631
+ }
1632
+ /** Published posts of one blog, newest first (featured first), paginated. */
1633
+ async posts(handle, query = {}) {
1634
+ return this.client.request("GET", `/blogs/${handle}/posts`, {
1635
+ query: {
1636
+ locale: query.locale,
1637
+ page: query.page,
1638
+ limit: query.limit,
1639
+ tag: query.tag
1640
+ }
1641
+ });
1642
+ }
1643
+ /** One published post with sanitised HTML content and related posts. */
1644
+ async post(handle, slug, locale) {
1645
+ return this.client.request("GET", `/blogs/${handle}/posts/${slug}`, {
1646
+ query: { locale }
1647
+ });
1648
+ }
1649
+ };
1650
+ var FormsModule = class {
1651
+ constructor(client) {
1652
+ this.client = client;
1653
+ }
1654
+ /** Public definition of one form (fields + settings). 404 for an unknown or inactive slug. */
1655
+ async get(slug) {
1656
+ return this.client.request(
1657
+ "GET",
1658
+ `/forms/${encodeURIComponent(slug)}`
1659
+ );
1660
+ }
1661
+ /**
1662
+ * Submit a response. On `be.forms.validationFailed` the returned error
1663
+ * carries per-field codes: read them with `formFieldErrors(error)`. Other
1664
+ * rejections: `be.forms.consentRequired`, `be.forms.tooLarge`,
1665
+ * `be.forms.formNotFound`; rate limit 10 submits per minute per visitor.
1666
+ */
1667
+ async submit(slug, input) {
1668
+ return this.client.request(
1669
+ "POST",
1670
+ `/forms/${encodeURIComponent(slug)}/submit`,
1671
+ { body: input, auth: false }
1672
+ );
1673
+ }
1674
+ };
1513
1675
  var WishlistModule = class {
1514
1676
  constructor(client) {
1515
1677
  this.client = client;
@@ -1659,7 +1821,7 @@ var ConsentModule = class {
1659
1821
  }
1660
1822
  if (result.error) return { data: null, error: result.error };
1661
1823
  return {
1662
- data: _optionalChain([result, 'access', _35 => _35.data, 'optionalAccess', _36 => _36.consented]) ? result.data.consent : null,
1824
+ data: _optionalChain([result, 'access', _39 => _39.data, 'optionalAccess', _40 => _40.consented]) ? result.data.consent : null,
1663
1825
  error: null
1664
1826
  };
1665
1827
  }
@@ -1746,10 +1908,10 @@ var ShippingModule = class {
1746
1908
  */
1747
1909
  async listMethods(opts) {
1748
1910
  const query = {};
1749
- if (_optionalChain([opts, 'optionalAccess', _37 => _37.currency])) query.currency = opts.currency;
1750
- if (_optionalChain([opts, 'optionalAccess', _38 => _38.country])) query.country = opts.country;
1751
- if (_optionalChain([opts, 'optionalAccess', _39 => _39.cartTotal]) != null) query.cartTotal = String(opts.cartTotal);
1752
- if (_optionalChain([opts, 'optionalAccess', _40 => _40.cartWeightKg]) != null)
1911
+ if (_optionalChain([opts, 'optionalAccess', _41 => _41.currency])) query.currency = opts.currency;
1912
+ if (_optionalChain([opts, 'optionalAccess', _42 => _42.country])) query.country = opts.country;
1913
+ if (_optionalChain([opts, 'optionalAccess', _43 => _43.cartTotal]) != null) query.cartTotal = String(opts.cartTotal);
1914
+ if (_optionalChain([opts, 'optionalAccess', _44 => _44.cartWeightKg]) != null)
1753
1915
  query.cartWeightKg = String(opts.cartWeightKg);
1754
1916
  return this.client.request(
1755
1917
  "GET",
@@ -33,6 +33,38 @@ var AddressTypes = {
33
33
  SHIPPING: "SHIPPING",
34
34
  BILLING: "BILLING"
35
35
  };
36
+ var BE_CODE_TO_ERROR = {
37
+ "be.storefront.cartIsEmpty": "CART_EMPTY",
38
+ "be.storefront.productNotFound": "PRODUCT_NOT_FOUND",
39
+ "be.storefront.productNotAvailable": "PRODUCT_NOT_FOUND",
40
+ "be.storefront.invalidEmailOrPassword": "INVALID_CREDENTIALS",
41
+ "be.storefront.currentPasswordIncorrect": "INVALID_CREDENTIALS",
42
+ "be.storefront.discountExpired": "DISCOUNT_EXPIRED",
43
+ "be.storefront.discountExpiredOrLimit": "DISCOUNT_EXPIRED",
44
+ "be.storefront.invalidDiscountCode": "INVALID_DISCOUNT",
45
+ "be.storefront.discountInvalid": "INVALID_DISCOUNT",
46
+ "be.storefront.discountInactive": "INVALID_DISCOUNT",
47
+ "be.storefront.discountWrongCurrency": "INVALID_DISCOUNT",
48
+ "be.storefront.discountNotApplicable": "INVALID_DISCOUNT",
49
+ "be.storefront.discountNotYetValid": "INVALID_DISCOUNT",
50
+ "be.storefront.discountUsageLimitReached": "INVALID_DISCOUNT",
51
+ "be.storefront.discountLimitedToGroups": "INVALID_DISCOUNT",
52
+ "be.storefront.tokenInvalid": "TOKEN_INVALID",
53
+ "be.storefront.orderAccessTokenInvalid": "TOKEN_INVALID",
54
+ "be.storefront.resetTokenInvalid": "TOKEN_INVALID",
55
+ "be.storefront.invalidVerificationToken": "TOKEN_INVALID",
56
+ "be.storefront.invalidRefreshToken": "TOKEN_INVALID",
57
+ "be.storefront.refreshTokenExpired": "TOKEN_EXPIRED",
58
+ "be.storefront.apiKeyExpired": "TOKEN_EXPIRED",
59
+ "be.storefront.customerEmailTaken": "EMAIL_ALREADY_EXISTS",
60
+ "be.storefront.orderNotCancellable": "ORDER_NOT_CANCELLABLE",
61
+ "be.storefront.orderCancelledNotReturnable": "ORDER_NOT_CANCELLABLE",
62
+ "be.validation.failed": "VALIDATION_ERROR",
63
+ "be.forms.formNotFound": "NOT_FOUND",
64
+ "be.forms.validationFailed": "VALIDATION_ERROR",
65
+ "be.forms.consentRequired": "VALIDATION_ERROR",
66
+ "be.forms.tooLarge": "VALIDATION_ERROR"
67
+ };
36
68
  var BehioApiError = class _BehioApiError extends Error {
37
69
  constructor(status, body, message) {
38
70
  super(message || `API Error ${status}`);
@@ -49,6 +81,11 @@ var BehioApiError = class _BehioApiError extends Error {
49
81
  if (status === 409) return "EMAIL_ALREADY_EXISTS";
50
82
  if (status === 429) return "RATE_LIMITED";
51
83
  if (status >= 500) return "INTERNAL_ERROR";
84
+ const rawCode = body?.code;
85
+ if (typeof rawCode === "string") {
86
+ const mapped = BE_CODE_TO_ERROR[rawCode];
87
+ if (mapped) return mapped;
88
+ }
52
89
  const msg = (body?.message || "").toLowerCase();
53
90
  if (msg.includes("invalid") && msg.includes("password"))
54
91
  return "INVALID_CREDENTIALS";
@@ -157,6 +194,8 @@ var BehioStorefront = class {
157
194
  this.orders = new OrdersModule(this);
158
195
  this.customer = new CustomerModule(this);
159
196
  this.pages = new PagesModule(this);
197
+ this.blog = new BlogModule(this);
198
+ this.forms = new FormsModule(this);
160
199
  this.wishlist = new WishlistModule(this);
161
200
  this.reviews = new ReviewsModule(this);
162
201
  this.returns = new ReturnsModule(this);
@@ -225,6 +264,21 @@ var BehioStorefront = class {
225
264
  body: input
226
265
  });
227
266
  }
267
+ /**
268
+ * Visitor messages from merchant automations (storefront.event action).
269
+ * Consent-gated visitor id; each message carries a merchant-defined `name`
270
+ * and free-form `payload` the template reacts to (modal, banner, ...).
271
+ * Messages stay listed until acknowledged via `ackVisitorMessage`.
272
+ */
273
+ async getVisitorMessages(visitorId) {
274
+ return this.request("GET", "/messages", { query: { visitorId } });
275
+ }
276
+ /** Acknowledge a visitor message so it is not delivered again. */
277
+ async ackVisitorMessage(messageId, visitorId) {
278
+ return this.request("POST", `/messages/${messageId}/ack`, {
279
+ query: { visitorId }
280
+ });
281
+ }
228
282
  /** Get basic shop info */
229
283
  async getShopInfo() {
230
284
  return this.request("GET", "/shop");
@@ -844,16 +898,27 @@ var CatalogModule = class {
844
898
  { body: input }
845
899
  );
846
900
  }
847
- /** List configured payment methods (filtered by currency). */
901
+ /**
902
+ * List configured payment methods (filtered by currency). `locale` picks the
903
+ * language of `instruments[].label` / `swifts[].label` (SDK 1.14.0); pass the
904
+ * locale of the page so the instrument tiles read in the shopper's language.
905
+ */
848
906
  async listPaymentMethods(opts) {
849
907
  const query = {};
850
908
  if (opts?.currency) query.currency = opts.currency;
909
+ if (opts?.locale) query.locale = opts.locale;
910
+ if (opts?.country) query.country = opts.country;
911
+ if (opts?.shippingMethodId) query.shippingMethodId = opts.shippingMethodId;
851
912
  return this.client.request(
852
913
  "GET",
853
914
  "/catalog/payment-methods",
854
915
  { query }
855
916
  );
856
917
  }
918
+ /** Alias of `listPaymentMethods` (SDK 1.14.0). */
919
+ paymentMethods(opts) {
920
+ return this.listPaymentMethods(opts);
921
+ }
857
922
  };
858
923
  var AuthModule = class {
859
924
  constructor(client) {
@@ -1007,6 +1072,26 @@ var CartModule = class {
1007
1072
  this.client.emit("cart:updated", res.data);
1008
1073
  return res;
1009
1074
  }
1075
+ /**
1076
+ * Tell the cart where the order will ship (and, for B2B, the buyer's VAT
1077
+ * ID) so the VAT breakdown matches the checkout before the address form:
1078
+ * destination-country rate (OSS), 0 % export outside the EU, or reverse
1079
+ * charge for an EU business with a VIES-valid VAT ID. `cart.vatMode` says
1080
+ * which rule applied. SDK 1.17.0.
1081
+ *
1082
+ * ```ts
1083
+ * await client.cart.setDestination({ country: "SK" });
1084
+ * await client.cart.setDestination({ vatId: "SK2020000001" }); // "" clears
1085
+ * ```
1086
+ */
1087
+ async setDestination(input) {
1088
+ const res = await this.client.request("PUT", "/cart/destination", {
1089
+ body: input
1090
+ });
1091
+ if (res.error) return res;
1092
+ this.client.emit("cart:updated", res.data);
1093
+ return res;
1094
+ }
1010
1095
  /** Update item quantity */
1011
1096
  async updateQuantity(itemId, quantity) {
1012
1097
  const res = await this.client.request(
@@ -1158,6 +1243,30 @@ var OrdersModule = class {
1158
1243
  constructor(client) {
1159
1244
  this.client = client;
1160
1245
  }
1246
+ /**
1247
+ * Ask the backend to re-check this order's payment with the gateway.
1248
+ *
1249
+ * Call it on the thank-you page the customer lands on after paying, BEFORE
1250
+ * you read the order. Some gateways (Tatrapay+) have no server-to-server
1251
+ * notification at all, so the customer's return is the only fast way the
1252
+ * payment gets confirmed; for the others it is a safety net for a lost
1253
+ * notification.
1254
+ *
1255
+ * The response is deliberately opaque (`{ok: true}` every time, even for an
1256
+ * order number that does not exist): order numbers are sequential, so
1257
+ * anything else would turn this into a probe for other people's orders.
1258
+ * Read the actual state afterwards through a path that proves entitlement
1259
+ * ({@link get}, {@link track} or the guest access-code flow).
1260
+ *
1261
+ * Never throws for a missing order and never blocks the page: treat a
1262
+ * failure as "not confirmed yet", the backend poller catches up on its own.
1263
+ */
1264
+ async syncPaymentOnReturn(orderNumber) {
1265
+ return this.client.request(
1266
+ "POST",
1267
+ `/payments/return/${orderNumber}`
1268
+ );
1269
+ }
1161
1270
  /** List customer orders (requires auth) */
1162
1271
  async list(options) {
1163
1272
  return this.client.request(
@@ -1510,6 +1619,59 @@ var PagesModule = class {
1510
1619
  });
1511
1620
  }
1512
1621
  };
1622
+ var BlogModule = class {
1623
+ constructor(client) {
1624
+ this.client = client;
1625
+ }
1626
+ /** List the site's blogs (active only). */
1627
+ async list(locale) {
1628
+ return this.client.request("GET", "/blogs", {
1629
+ query: { locale }
1630
+ });
1631
+ }
1632
+ /** Published posts of one blog, newest first (featured first), paginated. */
1633
+ async posts(handle, query = {}) {
1634
+ return this.client.request("GET", `/blogs/${handle}/posts`, {
1635
+ query: {
1636
+ locale: query.locale,
1637
+ page: query.page,
1638
+ limit: query.limit,
1639
+ tag: query.tag
1640
+ }
1641
+ });
1642
+ }
1643
+ /** One published post with sanitised HTML content and related posts. */
1644
+ async post(handle, slug, locale) {
1645
+ return this.client.request("GET", `/blogs/${handle}/posts/${slug}`, {
1646
+ query: { locale }
1647
+ });
1648
+ }
1649
+ };
1650
+ var FormsModule = class {
1651
+ constructor(client) {
1652
+ this.client = client;
1653
+ }
1654
+ /** Public definition of one form (fields + settings). 404 for an unknown or inactive slug. */
1655
+ async get(slug) {
1656
+ return this.client.request(
1657
+ "GET",
1658
+ `/forms/${encodeURIComponent(slug)}`
1659
+ );
1660
+ }
1661
+ /**
1662
+ * Submit a response. On `be.forms.validationFailed` the returned error
1663
+ * carries per-field codes: read them with `formFieldErrors(error)`. Other
1664
+ * rejections: `be.forms.consentRequired`, `be.forms.tooLarge`,
1665
+ * `be.forms.formNotFound`; rate limit 10 submits per minute per visitor.
1666
+ */
1667
+ async submit(slug, input) {
1668
+ return this.client.request(
1669
+ "POST",
1670
+ `/forms/${encodeURIComponent(slug)}/submit`,
1671
+ { body: input, auth: false }
1672
+ );
1673
+ }
1674
+ };
1513
1675
  var WishlistModule = class {
1514
1676
  constructor(client) {
1515
1677
  this.client = client;