@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/{chunk-YDZ2OF4O.mjs → chunk-25UM2XCS.mjs} +4 -0
- package/dist/{chunk-SBPQZG4Y.js → chunk-OUPCGMSR.js} +4 -0
- package/dist/{client-CAPtdqzd.d.mts → client-DtAz0rDJ.d.mts} +37 -1
- package/dist/{client-CAPtdqzd.d.ts → client-DtAz0rDJ.d.ts} +37 -1
- package/dist/index.d.mts +38 -4
- package/dist/index.d.ts +38 -4
- package/dist/index.js +152 -3
- package/dist/index.mjs +150 -1
- package/dist/next.d.mts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.js +2 -2
- package/dist/next.mjs +1 -1
- package/dist/react.d.mts +62 -2
- package/dist/react.d.ts +62 -2
- package/dist/react.js +294 -2
- package/dist/react.mjs +292 -2
- package/package.json +1 -1
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,
|