@behio/storefront-sdk 2.29.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,
@@ -2049,6 +2051,10 @@ var FormsModule = class {
2049
2051
  constructor(client) {
2050
2052
  this.client = client;
2051
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
+ }
2052
2058
  /** Public definition of one form (fields + settings). 404 for an unknown or inactive slug. */
2053
2059
  async get(slug) {
2054
2060
  return this.client.request(
@@ -3984,12 +3990,119 @@ function useAnalyticsEvents() {
3984
3990
  };
3985
3991
  }
3986
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
+
3987
4097
  // src/react/hooks/use-newsletter.ts
3988
4098
  var import_react_query30 = require("@tanstack/react-query");
3989
4099
  function useNewsletterSubscribe() {
3990
4100
  const { client } = useBehio();
3991
4101
  return (0, import_react_query30.useMutation)({
3992
- mutationFn: (input) => unwrap(client.newsletter.subscribe(input))
4102
+ mutationFn: (input) => unwrap(client.newsletter.subscribe(input)),
4103
+ onSuccess: (result) => {
4104
+ if (result.success) markNewsletterRequested();
4105
+ }
3993
4106
  });
3994
4107
  }
3995
4108
  function useNewsletterUnsubscribe() {
@@ -4341,7 +4454,10 @@ function useSiteForm(slug, options) {
4341
4454
  function useSiteFormSubmit(slug) {
4342
4455
  const { client } = useBehio();
4343
4456
  const mutation = (0, import_react_query39.useMutation)({
4344
- 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
+ }
4345
4461
  });
4346
4462
  const sdkError = mutation.error instanceof UnwrappedError ? mutation.error.sdkError : null;
4347
4463
  const fieldErrors = (0, import_react17.useMemo)(() => {
@@ -4374,6 +4490,10 @@ function useSiteFormSubmit(slug) {
4374
4490
  reset: mutation.reset
4375
4491
  };
4376
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
+ }
4377
4497
 
4378
4498
  // src/react/hooks/use-bookings.ts
4379
4499
  var import_react_query40 = require("@tanstack/react-query");
@@ -5228,6 +5348,176 @@ async function revokeAnalyticsConsent(client) {
5228
5348
  if (!id) return { data: { success: true }, error: null };
5229
5349
  return client.consent.revoke(id);
5230
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
+ }
5231
5521
  // Annotate the CommonJS export names for ESM import in node:
5232
5522
  0 && (module.exports = {
5233
5523
  BehioAnalyticsTracker,
@@ -5282,6 +5572,8 @@ async function revokeAnalyticsConsent(client) {
5282
5572
  useFaqGroups,
5283
5573
  useFeatured,
5284
5574
  useFilters,
5575
+ useFormPlacements,
5576
+ useFormPopup,
5285
5577
  useGiftCardBalance,
5286
5578
  useIsInWishlist,
5287
5579
  useLabels,
package/dist/react.mjs CHANGED
@@ -1925,6 +1925,10 @@ var FormsModule = class {
1925
1925
  constructor(client) {
1926
1926
  this.client = client;
1927
1927
  }
1928
+ /** Only active, published placements. Internal delivery/CRM settings never leave the server. */
1929
+ async placements() {
1930
+ return this.client.request("GET", "/forms");
1931
+ }
1928
1932
  /** Public definition of one form (fields + settings). 404 for an unknown or inactive slug. */
1929
1933
  async get(slug) {
1930
1934
  return this.client.request(
@@ -3860,12 +3864,119 @@ function useAnalyticsEvents() {
3860
3864
  };
3861
3865
  }
3862
3866
 
3867
+ // src/form-popup.ts
3868
+ function preferenceMemory() {
3869
+ if (typeof window === "undefined") return /* @__PURE__ */ new Map();
3870
+ const browser = window;
3871
+ return browser.__behioFormPreferences ?? (browser.__behioFormPreferences = /* @__PURE__ */ new Map());
3872
+ }
3873
+ var memoryAdapter = { get: (k) => preferenceMemory().get(k) ?? null, set: (k, v) => {
3874
+ preferenceMemory().set(k, v);
3875
+ }, remove: (k) => {
3876
+ preferenceMemory().delete(k);
3877
+ } };
3878
+ var NEWSLETTER_REQUESTED_EVENT = "behio:newsletter-requested";
3879
+ var PREFIX = "behio:forms:";
3880
+ var VISIT_MS = 30 * 60 * 1e3;
3881
+ function sessionAdapter() {
3882
+ return { get: (key) => {
3883
+ try {
3884
+ return sessionStorage.getItem(key) ?? memoryAdapter.get(key);
3885
+ } catch {
3886
+ return memoryAdapter.get(key);
3887
+ }
3888
+ }, set: (key, value) => {
3889
+ memoryAdapter.set(key, value);
3890
+ try {
3891
+ sessionStorage.setItem(key, value);
3892
+ } catch {
3893
+ }
3894
+ }, remove: (key) => {
3895
+ memoryAdapter.remove(key);
3896
+ try {
3897
+ sessionStorage.removeItem(key);
3898
+ } catch {
3899
+ }
3900
+ } };
3901
+ }
3902
+ function markNewsletterRequested() {
3903
+ if (typeof window === "undefined") return;
3904
+ memoryAdapter.set(PREFIX + "newsletter", "1");
3905
+ localStorageAdapter.set(PREFIX + "newsletter", "1");
3906
+ if (typeof window !== "undefined") window.dispatchEvent(new Event(NEWSLETTER_REQUESTED_EVENT));
3907
+ }
3908
+ function newsletterWasRequested() {
3909
+ return memoryAdapter.get(PREFIX + "newsletter") === "1" || localStorageAdapter.get(PREFIX + "newsletter") === "1";
3910
+ }
3911
+ function matchesFormPath(rule, pathname) {
3912
+ return rule.endsWith("*") ? pathname.startsWith(rule.slice(0, -1)) : pathname === rule;
3913
+ }
3914
+ function formPopupRouteAllowed(pathname, placement) {
3915
+ if (/^\/(cart|checkout|account|order|newsletter|login|register|forgot-password|reset-password|verify-email|session)(\/|$)/.test(pathname)) return false;
3916
+ return (!placement.includePaths.length || placement.includePaths.some((rule) => matchesFormPath(rule, pathname))) && !placement.excludePaths.some((rule) => matchesFormPath(rule, pathname));
3917
+ }
3918
+ var FormPopupClock = class {
3919
+ constructor(elapsedMs = 0) {
3920
+ this.since = null;
3921
+ this.elapsedMs = Math.max(0, elapsedMs);
3922
+ }
3923
+ setVisible(visible, now) {
3924
+ if (this.since !== null) this.elapsedMs += Math.max(0, now - this.since);
3925
+ this.since = visible ? now : null;
3926
+ }
3927
+ elapsed(now) {
3928
+ return this.elapsedMs + (this.since === null ? 0 : Math.max(0, now - this.since));
3929
+ }
3930
+ };
3931
+ var FormPopupPreferences = class {
3932
+ constructor(session = sessionAdapter(), persistent = localStorageAdapter, now = () => Date.now()) {
3933
+ this.session = session;
3934
+ this.persistent = persistent;
3935
+ this.now = now;
3936
+ this.shownInVisit = this.number("shown", this.session) > this.now() || this.number("shown", this.persistent) > this.now();
3937
+ }
3938
+ number(key, storage) {
3939
+ const n = Number(storage.get(PREFIX + key));
3940
+ return Number.isFinite(n) && n >= 0 ? n : 0;
3941
+ }
3942
+ visibleMs() {
3943
+ return this.now() - this.number("activity", this.session) < VISIT_MS ? this.number("visible", this.session) : 0;
3944
+ }
3945
+ saveTime(visible) {
3946
+ if (this.shownInVisit) {
3947
+ const until = String(this.now() + VISIT_MS);
3948
+ this.session.set(PREFIX + "shown", until);
3949
+ this.persistent.set(PREFIX + "shown", until);
3950
+ }
3951
+ this.session.set(PREFIX + "visible", String(visible));
3952
+ this.session.set(PREFIX + "activity", String(this.now()));
3953
+ }
3954
+ suppressed(slug, newsletter = false) {
3955
+ 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();
3956
+ }
3957
+ shown() {
3958
+ this.shownInVisit = true;
3959
+ const until = String(this.now() + VISIT_MS);
3960
+ this.session.set(PREFIX + "shown", until);
3961
+ this.persistent.set(PREFIX + "shown", until);
3962
+ }
3963
+ dismiss(slug, days) {
3964
+ const key = PREFIX + "dismissed:" + slug;
3965
+ const until = String(this.now() + days * 864e5);
3966
+ this.session.set(key, until);
3967
+ this.persistent.set(key, until);
3968
+ }
3969
+ };
3970
+
3863
3971
  // src/react/hooks/use-newsletter.ts
3864
3972
  import { useMutation as useMutation11 } from "@tanstack/react-query";
3865
3973
  function useNewsletterSubscribe() {
3866
3974
  const { client } = useBehio();
3867
3975
  return useMutation11({
3868
- mutationFn: (input) => unwrap(client.newsletter.subscribe(input))
3976
+ mutationFn: (input) => unwrap(client.newsletter.subscribe(input)),
3977
+ onSuccess: (result) => {
3978
+ if (result.success) markNewsletterRequested();
3979
+ }
3869
3980
  });
3870
3981
  }
3871
3982
  function useNewsletterUnsubscribe() {
@@ -4217,7 +4328,10 @@ function useSiteForm(slug, options) {
4217
4328
  function useSiteFormSubmit(slug) {
4218
4329
  const { client } = useBehio();
4219
4330
  const mutation = useMutation15({
4220
- mutationFn: (input) => unwrap(client.forms.submit(slug, input))
4331
+ mutationFn: (input) => unwrap(client.forms.submit(slug, input)),
4332
+ onSuccess: (result) => {
4333
+ if (typeof result.confirmationRequired === "boolean") markNewsletterRequested();
4334
+ }
4221
4335
  });
4222
4336
  const sdkError = mutation.error instanceof UnwrappedError ? mutation.error.sdkError : null;
4223
4337
  const fieldErrors = useMemo4(() => {
@@ -4250,6 +4364,10 @@ function useSiteFormSubmit(slug) {
4250
4364
  reset: mutation.reset
4251
4365
  };
4252
4366
  }
4367
+ function useFormPlacements(options) {
4368
+ const { client } = useBehio();
4369
+ return useQuery33({ queryKey: ["behio", "form-placements"], queryFn: () => unwrap(client.forms.placements()), initialData: options?.initialData, enabled: options?.enabled !== false, staleTime: 3e5 });
4370
+ }
4253
4371
 
4254
4372
  // src/react/hooks/use-bookings.ts
4255
4373
  import { useMutation as useMutation16, useQuery as useQuery34, useQueryClient as useQueryClient16 } from "@tanstack/react-query";
@@ -5104,6 +5222,176 @@ async function revokeAnalyticsConsent(client) {
5104
5222
  if (!id) return { data: { success: true }, error: null };
5105
5223
  return client.consent.revoke(id);
5106
5224
  }
5225
+
5226
+ // src/react/hooks/use-form-popup.ts
5227
+ import { useEffect as useEffect9, useRef as useRef5, useState as useState11 } from "react";
5228
+ var MODALS = '[aria-modal="true"], dialog[open], [data-consent-banner]';
5229
+ function blocked(own) {
5230
+ return Array.from(document.querySelectorAll(MODALS)).some((element) => element !== own && element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden");
5231
+ }
5232
+ function publicPath(locales) {
5233
+ const path = window.location.pathname;
5234
+ const first = path.split("/")[1];
5235
+ return locales.includes(first) ? path.slice(first.length + 1) || "/" : path;
5236
+ }
5237
+ function useFormPopup({ forms, locales, pathname }) {
5238
+ const { client } = useBehio();
5239
+ const [active, setActive] = useState11(null);
5240
+ const dialog = useRef5(null);
5241
+ const current = useRef5({ forms, locales });
5242
+ current.current = { forms, locales };
5243
+ const preferences = useRef5(null);
5244
+ const evaluateRef = useRef5(null);
5245
+ const shownForm = useRef5(active);
5246
+ shownForm.current = active;
5247
+ const closed = useRef5(false);
5248
+ const mountedOffer = useRef5(null);
5249
+ const returnFocus = useRef5(null);
5250
+ useEffect9(() => {
5251
+ evaluateRef.current?.();
5252
+ }, [pathname, forms, locales]);
5253
+ useEffect9(() => {
5254
+ const prefs = preferences.current = new FormPopupPreferences();
5255
+ const clock = new FormPopupClock(prefs.visibleMs());
5256
+ clock.setVisible(!document.hidden, performance.now());
5257
+ let timer;
5258
+ let disposed = false, opening = false, shownHere = false;
5259
+ const flush = () => prefs.saveTime(clock.elapsed(performance.now()));
5260
+ const evaluate = async () => {
5261
+ clearTimeout(timer);
5262
+ if (disposed || opening || shownHere || document.hidden) return;
5263
+ const elapsed = clock.elapsed(performance.now());
5264
+ const candidates = current.current.forms.flatMap((form) => (form.presentation?.placements ?? []).filter((placement) => placement.mode === "popup" && placement.enabled).map((placement) => ({ form, placement })));
5265
+ let nextDelay = Infinity;
5266
+ for (const candidate of candidates) {
5267
+ const { form, placement } = candidate;
5268
+ if (!form.revision || prefs.suppressed(form.slug, form.presentation?.purpose === "newsletter")) continue;
5269
+ if (!formPopupRouteAllowed(publicPath(current.current.locales), placement)) continue;
5270
+ const mobile = window.matchMedia("(max-width: 767px)").matches;
5271
+ if (placement.devices !== "all" && placement.devices === "mobile" !== mobile) continue;
5272
+ const remaining = placement.delaySeconds * 1e3 - elapsed;
5273
+ if (remaining > 0) {
5274
+ nextDelay = Math.min(nextDelay, remaining);
5275
+ continue;
5276
+ }
5277
+ if (blocked(dialog.current)) continue;
5278
+ opening = true;
5279
+ try {
5280
+ const fresh = await client.forms.get(form.slug);
5281
+ if (disposed || document.hidden || fresh.error || fresh.data?.revision !== form.revision || blocked(dialog.current) || !formPopupRouteAllowed(publicPath(current.current.locales), placement)) return;
5282
+ const open = async () => {
5283
+ if (disposed || prefs.suppressed(form.slug, form.presentation?.purpose === "newsletter") || blocked(dialog.current)) return false;
5284
+ closed.current = false;
5285
+ returnFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
5286
+ return new Promise((resolve) => {
5287
+ mountedOffer.current = resolve;
5288
+ setActive(candidate);
5289
+ });
5290
+ };
5291
+ const opened = navigator.locks ? await navigator.locks.request("behio-form-popup", { ifAvailable: true }, async (lock) => lock ? await open() : false) : await open();
5292
+ if (opened) {
5293
+ shownHere = true;
5294
+ flush();
5295
+ }
5296
+ } catch {
5297
+ } finally {
5298
+ opening = false;
5299
+ }
5300
+ return;
5301
+ }
5302
+ if (Number.isFinite(nextDelay)) timer = setTimeout(() => void evaluate(), Math.max(1, nextDelay));
5303
+ };
5304
+ evaluateRef.current = () => {
5305
+ void evaluate();
5306
+ };
5307
+ const visibility = () => {
5308
+ clock.setVisible(!document.hidden, performance.now());
5309
+ flush();
5310
+ void evaluate();
5311
+ };
5312
+ const changed = () => {
5313
+ if (dialog.current?.open && (blocked(dialog.current) || shownForm.current?.form.presentation?.purpose === "newsletter" && newsletterWasRequested() && !dialog.current.querySelector('[role="status"]'))) setActive(null);
5314
+ void evaluate();
5315
+ };
5316
+ const observer = new MutationObserver(changed);
5317
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["open", "aria-modal", "hidden", "style", "class"] });
5318
+ document.addEventListener("visibilitychange", visibility);
5319
+ window.addEventListener("pagehide", flush);
5320
+ window.addEventListener("storage", changed);
5321
+ window.addEventListener("resize", changed);
5322
+ window.addEventListener(NEWSLETTER_REQUESTED_EVENT, changed);
5323
+ void evaluate();
5324
+ return () => {
5325
+ flush();
5326
+ disposed = true;
5327
+ mountedOffer.current?.(false);
5328
+ mountedOffer.current = null;
5329
+ clearTimeout(timer);
5330
+ observer.disconnect();
5331
+ evaluateRef.current = null;
5332
+ document.removeEventListener("visibilitychange", visibility);
5333
+ window.removeEventListener("pagehide", flush);
5334
+ window.removeEventListener("storage", changed);
5335
+ window.removeEventListener("resize", changed);
5336
+ window.removeEventListener(NEWSLETTER_REQUESTED_EVENT, changed);
5337
+ };
5338
+ }, [client]);
5339
+ useEffect9(() => {
5340
+ if (!active || !dialog.current) return;
5341
+ const node = dialog.current, focused = returnFocus.current;
5342
+ const overflow = document.body.style.overflow;
5343
+ if (document.hidden || blocked(node)) {
5344
+ mountedOffer.current?.(false);
5345
+ mountedOffer.current = null;
5346
+ setActive(null);
5347
+ return;
5348
+ }
5349
+ try {
5350
+ node.showModal();
5351
+ } catch {
5352
+ mountedOffer.current?.(false);
5353
+ mountedOffer.current = null;
5354
+ setActive(null);
5355
+ return;
5356
+ }
5357
+ document.body.style.overflow = "hidden";
5358
+ const trapTab = (event) => {
5359
+ if (event.key !== "Tab") return;
5360
+ 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);
5361
+ const first = controls[0], last = controls[controls.length - 1];
5362
+ if (!first || !last) return;
5363
+ if (event.shiftKey && (document.activeElement === first || !node.contains(document.activeElement))) {
5364
+ event.preventDefault();
5365
+ last.focus();
5366
+ } else if (!event.shiftKey && (document.activeElement === last || !node.contains(document.activeElement))) {
5367
+ event.preventDefault();
5368
+ first.focus();
5369
+ }
5370
+ };
5371
+ node.addEventListener("keydown", trapTab);
5372
+ preferences.current?.shown();
5373
+ mountedOffer.current?.(true);
5374
+ mountedOffer.current = null;
5375
+ trackEcommerceEvent("form_popup_view", { props: { form: active.form.slug, placement: active.placement.key, revision: active.form.revision } });
5376
+ return () => {
5377
+ closed.current = true;
5378
+ node.removeEventListener("keydown", trapTab);
5379
+ if (node.open) node.close();
5380
+ document.body.style.overflow = overflow;
5381
+ if (focused?.isConnected) focused.focus({ preventScroll: true });
5382
+ };
5383
+ }, [active]);
5384
+ useEffect9(() => {
5385
+ if (active && !formPopupRouteAllowed(publicPath(locales), active.placement)) setActive(null);
5386
+ }, [pathname, active, locales]);
5387
+ const dismiss = () => {
5388
+ if (!active || closed.current) return;
5389
+ closed.current = true;
5390
+ preferences.current?.dismiss(active.form.slug, active.placement.dismissDays);
5391
+ setActive(null);
5392
+ };
5393
+ return { active, dialog, dismiss };
5394
+ }
5107
5395
  export {
5108
5396
  BehioAnalyticsTracker,
5109
5397
  BehioProvider,
@@ -5157,6 +5445,8 @@ export {
5157
5445
  useFaqGroups,
5158
5446
  useFeatured,
5159
5447
  useFilters,
5448
+ useFormPlacements,
5449
+ useFormPopup,
5160
5450
  useGiftCardBalance,
5161
5451
  useIsInWishlist,
5162
5452
  useLabels,