@behio/storefront-sdk 2.28.0 → 2.30.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/react.js CHANGED
@@ -73,6 +73,8 @@ __export(react_exports, {
73
73
  useFaqGroups: () => useFaqGroups,
74
74
  useFeatured: () => useFeatured,
75
75
  useFilters: () => useFilters,
76
+ useFormPlacements: () => useFormPlacements,
77
+ useFormPopup: () => useFormPopup,
76
78
  useGiftCardBalance: () => useGiftCardBalance,
77
79
  useIsInWishlist: () => useIsInWishlist,
78
80
  useLabels: () => useLabels,
@@ -85,6 +87,7 @@ __export(react_exports, {
85
87
  useManagedBooking: () => useManagedBooking,
86
88
  useManagedBookingSlots: () => useManagedBookingSlots,
87
89
  useMenu: () => useMenu,
90
+ useNewsletterConfirm: () => useNewsletterConfirm,
88
91
  useNewsletterSubscribe: () => useNewsletterSubscribe,
89
92
  useNewsletterUnsubscribe: () => useNewsletterUnsubscribe,
90
93
  useNotifyWhenAvailable: () => useNotifyWhenAvailable,
@@ -143,6 +146,7 @@ var BE_CODE_TO_ERROR = {
143
146
  "be.storefront.discountNotApplicable": "INVALID_DISCOUNT",
144
147
  "be.storefront.discountNotYetValid": "INVALID_DISCOUNT",
145
148
  "be.storefront.discountUsageLimitReached": "INVALID_DISCOUNT",
149
+ "be.storefront.personalDiscountNotEligible": "INVALID_DISCOUNT",
146
150
  "be.storefront.discountLimitedToGroups": "INVALID_DISCOUNT",
147
151
  "be.storefront.tokenInvalid": "TOKEN_INVALID",
148
152
  "be.storefront.orderAccessTokenInvalid": "TOKEN_INVALID",
@@ -2047,6 +2051,10 @@ var FormsModule = class {
2047
2051
  constructor(client) {
2048
2052
  this.client = client;
2049
2053
  }
2054
+ /** Only active, published placements. Internal delivery/CRM settings never leave the server. */
2055
+ async placements() {
2056
+ return this.client.request("GET", "/forms");
2057
+ }
2050
2058
  /** Public definition of one form (fields + settings). 404 for an unknown or inactive slug. */
2051
2059
  async get(slug) {
2052
2060
  return this.client.request(
@@ -2407,6 +2415,12 @@ var NewsletterModule = class {
2407
2415
  }
2408
2416
  );
2409
2417
  }
2418
+ async confirm(token) {
2419
+ return this.client.request("POST", "/newsletter/confirm", {
2420
+ body: { token },
2421
+ auth: false
2422
+ });
2423
+ }
2410
2424
  async unsubscribe(email) {
2411
2425
  return this.client.request(
2412
2426
  "POST",
@@ -3976,12 +3990,119 @@ function useAnalyticsEvents() {
3976
3990
  };
3977
3991
  }
3978
3992
 
3993
+ // src/form-popup.ts
3994
+ function preferenceMemory() {
3995
+ if (typeof window === "undefined") return /* @__PURE__ */ new Map();
3996
+ const browser = window;
3997
+ return browser.__behioFormPreferences ?? (browser.__behioFormPreferences = /* @__PURE__ */ new Map());
3998
+ }
3999
+ var memoryAdapter = { get: (k) => preferenceMemory().get(k) ?? null, set: (k, v) => {
4000
+ preferenceMemory().set(k, v);
4001
+ }, remove: (k) => {
4002
+ preferenceMemory().delete(k);
4003
+ } };
4004
+ var NEWSLETTER_REQUESTED_EVENT = "behio:newsletter-requested";
4005
+ var PREFIX = "behio:forms:";
4006
+ var VISIT_MS = 30 * 60 * 1e3;
4007
+ function sessionAdapter() {
4008
+ return { get: (key) => {
4009
+ try {
4010
+ return sessionStorage.getItem(key) ?? memoryAdapter.get(key);
4011
+ } catch {
4012
+ return memoryAdapter.get(key);
4013
+ }
4014
+ }, set: (key, value) => {
4015
+ memoryAdapter.set(key, value);
4016
+ try {
4017
+ sessionStorage.setItem(key, value);
4018
+ } catch {
4019
+ }
4020
+ }, remove: (key) => {
4021
+ memoryAdapter.remove(key);
4022
+ try {
4023
+ sessionStorage.removeItem(key);
4024
+ } catch {
4025
+ }
4026
+ } };
4027
+ }
4028
+ function markNewsletterRequested() {
4029
+ if (typeof window === "undefined") return;
4030
+ memoryAdapter.set(PREFIX + "newsletter", "1");
4031
+ localStorageAdapter.set(PREFIX + "newsletter", "1");
4032
+ if (typeof window !== "undefined") window.dispatchEvent(new Event(NEWSLETTER_REQUESTED_EVENT));
4033
+ }
4034
+ function newsletterWasRequested() {
4035
+ return memoryAdapter.get(PREFIX + "newsletter") === "1" || localStorageAdapter.get(PREFIX + "newsletter") === "1";
4036
+ }
4037
+ function matchesFormPath(rule, pathname) {
4038
+ return rule.endsWith("*") ? pathname.startsWith(rule.slice(0, -1)) : pathname === rule;
4039
+ }
4040
+ function formPopupRouteAllowed(pathname, placement) {
4041
+ if (/^\/(cart|checkout|account|order|newsletter|login|register|forgot-password|reset-password|verify-email|session)(\/|$)/.test(pathname)) return false;
4042
+ return (!placement.includePaths.length || placement.includePaths.some((rule) => matchesFormPath(rule, pathname))) && !placement.excludePaths.some((rule) => matchesFormPath(rule, pathname));
4043
+ }
4044
+ var FormPopupClock = class {
4045
+ constructor(elapsedMs = 0) {
4046
+ this.since = null;
4047
+ this.elapsedMs = Math.max(0, elapsedMs);
4048
+ }
4049
+ setVisible(visible, now) {
4050
+ if (this.since !== null) this.elapsedMs += Math.max(0, now - this.since);
4051
+ this.since = visible ? now : null;
4052
+ }
4053
+ elapsed(now) {
4054
+ return this.elapsedMs + (this.since === null ? 0 : Math.max(0, now - this.since));
4055
+ }
4056
+ };
4057
+ var FormPopupPreferences = class {
4058
+ constructor(session = sessionAdapter(), persistent = localStorageAdapter, now = () => Date.now()) {
4059
+ this.session = session;
4060
+ this.persistent = persistent;
4061
+ this.now = now;
4062
+ this.shownInVisit = this.number("shown", this.session) > this.now() || this.number("shown", this.persistent) > this.now();
4063
+ }
4064
+ number(key, storage) {
4065
+ const n = Number(storage.get(PREFIX + key));
4066
+ return Number.isFinite(n) && n >= 0 ? n : 0;
4067
+ }
4068
+ visibleMs() {
4069
+ return this.now() - this.number("activity", this.session) < VISIT_MS ? this.number("visible", this.session) : 0;
4070
+ }
4071
+ saveTime(visible) {
4072
+ if (this.shownInVisit) {
4073
+ const until = String(this.now() + VISIT_MS);
4074
+ this.session.set(PREFIX + "shown", until);
4075
+ this.persistent.set(PREFIX + "shown", until);
4076
+ }
4077
+ this.session.set(PREFIX + "visible", String(visible));
4078
+ this.session.set(PREFIX + "activity", String(this.now()));
4079
+ }
4080
+ suppressed(slug, newsletter = false) {
4081
+ return newsletter && newsletterWasRequested() || this.shownInVisit || this.number("shown", this.session) > this.now() || this.number("shown", this.persistent) > this.now() || this.number("dismissed:" + slug, this.persistent) > this.now() || this.number("dismissed:" + slug, this.session) > this.now();
4082
+ }
4083
+ shown() {
4084
+ this.shownInVisit = true;
4085
+ const until = String(this.now() + VISIT_MS);
4086
+ this.session.set(PREFIX + "shown", until);
4087
+ this.persistent.set(PREFIX + "shown", until);
4088
+ }
4089
+ dismiss(slug, days) {
4090
+ const key = PREFIX + "dismissed:" + slug;
4091
+ const until = String(this.now() + days * 864e5);
4092
+ this.session.set(key, until);
4093
+ this.persistent.set(key, until);
4094
+ }
4095
+ };
4096
+
3979
4097
  // src/react/hooks/use-newsletter.ts
3980
4098
  var import_react_query30 = require("@tanstack/react-query");
3981
4099
  function useNewsletterSubscribe() {
3982
4100
  const { client } = useBehio();
3983
4101
  return (0, import_react_query30.useMutation)({
3984
- mutationFn: (input) => unwrap(client.newsletter.subscribe(input))
4102
+ mutationFn: (input) => unwrap(client.newsletter.subscribe(input)),
4103
+ onSuccess: (result) => {
4104
+ if (result.success) markNewsletterRequested();
4105
+ }
3985
4106
  });
3986
4107
  }
3987
4108
  function useNewsletterUnsubscribe() {
@@ -3990,6 +4111,12 @@ function useNewsletterUnsubscribe() {
3990
4111
  mutationFn: (email) => unwrap(client.newsletter.unsubscribe(email))
3991
4112
  });
3992
4113
  }
4114
+ function useNewsletterConfirm() {
4115
+ const { client } = useBehio();
4116
+ return (0, import_react_query30.useMutation)({
4117
+ mutationFn: (token) => unwrap(client.newsletter.confirm(token))
4118
+ });
4119
+ }
3993
4120
 
3994
4121
  // src/react/hooks/use-orders.ts
3995
4122
  var import_react13 = require("react");
@@ -4327,7 +4454,10 @@ function useSiteForm(slug, options) {
4327
4454
  function useSiteFormSubmit(slug) {
4328
4455
  const { client } = useBehio();
4329
4456
  const mutation = (0, import_react_query39.useMutation)({
4330
- mutationFn: (input) => unwrap(client.forms.submit(slug, input))
4457
+ mutationFn: (input) => unwrap(client.forms.submit(slug, input)),
4458
+ onSuccess: (result) => {
4459
+ if (typeof result.confirmationRequired === "boolean") markNewsletterRequested();
4460
+ }
4331
4461
  });
4332
4462
  const sdkError = mutation.error instanceof UnwrappedError ? mutation.error.sdkError : null;
4333
4463
  const fieldErrors = (0, import_react17.useMemo)(() => {
@@ -4360,6 +4490,10 @@ function useSiteFormSubmit(slug) {
4360
4490
  reset: mutation.reset
4361
4491
  };
4362
4492
  }
4493
+ function useFormPlacements(options) {
4494
+ const { client } = useBehio();
4495
+ return (0, import_react_query39.useQuery)({ queryKey: ["behio", "form-placements"], queryFn: () => unwrap(client.forms.placements()), initialData: options?.initialData, enabled: options?.enabled !== false, staleTime: 3e5 });
4496
+ }
4363
4497
 
4364
4498
  // src/react/hooks/use-bookings.ts
4365
4499
  var import_react_query40 = require("@tanstack/react-query");
@@ -5214,6 +5348,176 @@ async function revokeAnalyticsConsent(client) {
5214
5348
  if (!id) return { data: { success: true }, error: null };
5215
5349
  return client.consent.revoke(id);
5216
5350
  }
5351
+
5352
+ // src/react/hooks/use-form-popup.ts
5353
+ var import_react21 = require("react");
5354
+ var MODALS = '[aria-modal="true"], dialog[open], [data-consent-banner]';
5355
+ function blocked(own) {
5356
+ return Array.from(document.querySelectorAll(MODALS)).some((element) => element !== own && element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden");
5357
+ }
5358
+ function publicPath(locales) {
5359
+ const path = window.location.pathname;
5360
+ const first = path.split("/")[1];
5361
+ return locales.includes(first) ? path.slice(first.length + 1) || "/" : path;
5362
+ }
5363
+ function useFormPopup({ forms, locales, pathname }) {
5364
+ const { client } = useBehio();
5365
+ const [active, setActive] = (0, import_react21.useState)(null);
5366
+ const dialog = (0, import_react21.useRef)(null);
5367
+ const current = (0, import_react21.useRef)({ forms, locales });
5368
+ current.current = { forms, locales };
5369
+ const preferences = (0, import_react21.useRef)(null);
5370
+ const evaluateRef = (0, import_react21.useRef)(null);
5371
+ const shownForm = (0, import_react21.useRef)(active);
5372
+ shownForm.current = active;
5373
+ const closed = (0, import_react21.useRef)(false);
5374
+ const mountedOffer = (0, import_react21.useRef)(null);
5375
+ const returnFocus = (0, import_react21.useRef)(null);
5376
+ (0, import_react21.useEffect)(() => {
5377
+ evaluateRef.current?.();
5378
+ }, [pathname, forms, locales]);
5379
+ (0, import_react21.useEffect)(() => {
5380
+ const prefs = preferences.current = new FormPopupPreferences();
5381
+ const clock = new FormPopupClock(prefs.visibleMs());
5382
+ clock.setVisible(!document.hidden, performance.now());
5383
+ let timer;
5384
+ let disposed = false, opening = false, shownHere = false;
5385
+ const flush = () => prefs.saveTime(clock.elapsed(performance.now()));
5386
+ const evaluate = async () => {
5387
+ clearTimeout(timer);
5388
+ if (disposed || opening || shownHere || document.hidden) return;
5389
+ const elapsed = clock.elapsed(performance.now());
5390
+ const candidates = current.current.forms.flatMap((form) => (form.presentation?.placements ?? []).filter((placement) => placement.mode === "popup" && placement.enabled).map((placement) => ({ form, placement })));
5391
+ let nextDelay = Infinity;
5392
+ for (const candidate of candidates) {
5393
+ const { form, placement } = candidate;
5394
+ if (!form.revision || prefs.suppressed(form.slug, form.presentation?.purpose === "newsletter")) continue;
5395
+ if (!formPopupRouteAllowed(publicPath(current.current.locales), placement)) continue;
5396
+ const mobile = window.matchMedia("(max-width: 767px)").matches;
5397
+ if (placement.devices !== "all" && placement.devices === "mobile" !== mobile) continue;
5398
+ const remaining = placement.delaySeconds * 1e3 - elapsed;
5399
+ if (remaining > 0) {
5400
+ nextDelay = Math.min(nextDelay, remaining);
5401
+ continue;
5402
+ }
5403
+ if (blocked(dialog.current)) continue;
5404
+ opening = true;
5405
+ try {
5406
+ const fresh = await client.forms.get(form.slug);
5407
+ if (disposed || document.hidden || fresh.error || fresh.data?.revision !== form.revision || blocked(dialog.current) || !formPopupRouteAllowed(publicPath(current.current.locales), placement)) return;
5408
+ const open = async () => {
5409
+ if (disposed || prefs.suppressed(form.slug, form.presentation?.purpose === "newsletter") || blocked(dialog.current)) return false;
5410
+ closed.current = false;
5411
+ returnFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
5412
+ return new Promise((resolve) => {
5413
+ mountedOffer.current = resolve;
5414
+ setActive(candidate);
5415
+ });
5416
+ };
5417
+ const opened = navigator.locks ? await navigator.locks.request("behio-form-popup", { ifAvailable: true }, async (lock) => lock ? await open() : false) : await open();
5418
+ if (opened) {
5419
+ shownHere = true;
5420
+ flush();
5421
+ }
5422
+ } catch {
5423
+ } finally {
5424
+ opening = false;
5425
+ }
5426
+ return;
5427
+ }
5428
+ if (Number.isFinite(nextDelay)) timer = setTimeout(() => void evaluate(), Math.max(1, nextDelay));
5429
+ };
5430
+ evaluateRef.current = () => {
5431
+ void evaluate();
5432
+ };
5433
+ const visibility = () => {
5434
+ clock.setVisible(!document.hidden, performance.now());
5435
+ flush();
5436
+ void evaluate();
5437
+ };
5438
+ const changed = () => {
5439
+ if (dialog.current?.open && (blocked(dialog.current) || shownForm.current?.form.presentation?.purpose === "newsletter" && newsletterWasRequested() && !dialog.current.querySelector('[role="status"]'))) setActive(null);
5440
+ void evaluate();
5441
+ };
5442
+ const observer = new MutationObserver(changed);
5443
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["open", "aria-modal", "hidden", "style", "class"] });
5444
+ document.addEventListener("visibilitychange", visibility);
5445
+ window.addEventListener("pagehide", flush);
5446
+ window.addEventListener("storage", changed);
5447
+ window.addEventListener("resize", changed);
5448
+ window.addEventListener(NEWSLETTER_REQUESTED_EVENT, changed);
5449
+ void evaluate();
5450
+ return () => {
5451
+ flush();
5452
+ disposed = true;
5453
+ mountedOffer.current?.(false);
5454
+ mountedOffer.current = null;
5455
+ clearTimeout(timer);
5456
+ observer.disconnect();
5457
+ evaluateRef.current = null;
5458
+ document.removeEventListener("visibilitychange", visibility);
5459
+ window.removeEventListener("pagehide", flush);
5460
+ window.removeEventListener("storage", changed);
5461
+ window.removeEventListener("resize", changed);
5462
+ window.removeEventListener(NEWSLETTER_REQUESTED_EVENT, changed);
5463
+ };
5464
+ }, [client]);
5465
+ (0, import_react21.useEffect)(() => {
5466
+ if (!active || !dialog.current) return;
5467
+ const node = dialog.current, focused = returnFocus.current;
5468
+ const overflow = document.body.style.overflow;
5469
+ if (document.hidden || blocked(node)) {
5470
+ mountedOffer.current?.(false);
5471
+ mountedOffer.current = null;
5472
+ setActive(null);
5473
+ return;
5474
+ }
5475
+ try {
5476
+ node.showModal();
5477
+ } catch {
5478
+ mountedOffer.current?.(false);
5479
+ mountedOffer.current = null;
5480
+ setActive(null);
5481
+ return;
5482
+ }
5483
+ document.body.style.overflow = "hidden";
5484
+ const trapTab = (event) => {
5485
+ if (event.key !== "Tab") return;
5486
+ const controls = Array.from(node.querySelectorAll("a[href],button,input,select,textarea,[tabindex]")).filter((element) => element.tabIndex >= 0 && !element.matches(":disabled") && element.getClientRects().length > 0);
5487
+ const first = controls[0], last = controls[controls.length - 1];
5488
+ if (!first || !last) return;
5489
+ if (event.shiftKey && (document.activeElement === first || !node.contains(document.activeElement))) {
5490
+ event.preventDefault();
5491
+ last.focus();
5492
+ } else if (!event.shiftKey && (document.activeElement === last || !node.contains(document.activeElement))) {
5493
+ event.preventDefault();
5494
+ first.focus();
5495
+ }
5496
+ };
5497
+ node.addEventListener("keydown", trapTab);
5498
+ preferences.current?.shown();
5499
+ mountedOffer.current?.(true);
5500
+ mountedOffer.current = null;
5501
+ trackEcommerceEvent("form_popup_view", { props: { form: active.form.slug, placement: active.placement.key, revision: active.form.revision } });
5502
+ return () => {
5503
+ closed.current = true;
5504
+ node.removeEventListener("keydown", trapTab);
5505
+ if (node.open) node.close();
5506
+ document.body.style.overflow = overflow;
5507
+ if (focused?.isConnected) focused.focus({ preventScroll: true });
5508
+ };
5509
+ }, [active]);
5510
+ (0, import_react21.useEffect)(() => {
5511
+ if (active && !formPopupRouteAllowed(publicPath(locales), active.placement)) setActive(null);
5512
+ }, [pathname, active, locales]);
5513
+ const dismiss = () => {
5514
+ if (!active || closed.current) return;
5515
+ closed.current = true;
5516
+ preferences.current?.dismiss(active.form.slug, active.placement.dismissDays);
5517
+ setActive(null);
5518
+ };
5519
+ return { active, dialog, dismiss };
5520
+ }
5217
5521
  // Annotate the CommonJS export names for ESM import in node:
5218
5522
  0 && (module.exports = {
5219
5523
  BehioAnalyticsTracker,
@@ -5268,6 +5572,8 @@ async function revokeAnalyticsConsent(client) {
5268
5572
  useFaqGroups,
5269
5573
  useFeatured,
5270
5574
  useFilters,
5575
+ useFormPlacements,
5576
+ useFormPopup,
5271
5577
  useGiftCardBalance,
5272
5578
  useIsInWishlist,
5273
5579
  useLabels,
@@ -5280,6 +5586,7 @@ async function revokeAnalyticsConsent(client) {
5280
5586
  useManagedBooking,
5281
5587
  useManagedBookingSlots,
5282
5588
  useMenu,
5589
+ useNewsletterConfirm,
5283
5590
  useNewsletterSubscribe,
5284
5591
  useNewsletterUnsubscribe,
5285
5592
  useNotifyWhenAvailable,