@aranova/tracking-react 1.2.5 → 1.3.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/client.js CHANGED
@@ -32,10 +32,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var client_exports = {};
33
33
  __export(client_exports, {
34
34
  AdPlatformTracking: () => AdPlatformTracking,
35
+ BookingHandoffLink: () => BookingHandoffLink,
35
36
  ConsentBanner: () => ConsentBanner,
36
37
  GoogleAdsTracking: () => GoogleAdsTracking,
37
38
  PhoneField: () => PhoneField,
38
39
  createTracking: () => createTracking,
40
+ useBookingHandoffUrl: () => useBookingHandoffUrl,
41
+ useBookingIntegration: () => useBookingIntegration,
42
+ useBookingSlots: () => useBookingSlots,
43
+ useBookingSubmit: () => useBookingSubmit,
39
44
  useConsent: () => useConsent,
40
45
  useConsentState: () => useConsentState,
41
46
  useCookiePreferences: () => useCookiePreferences,
@@ -289,6 +294,8 @@ var TRACKING_CAPABILITIES = [
289
294
  "ad_tags_meta",
290
295
  "base_tracking",
291
296
  "blog_rendering",
297
+ "booking_handoff",
298
+ "booking_integration",
292
299
  "calendar_read",
293
300
  "calendar_write",
294
301
  "consent_controls",
@@ -691,15 +698,23 @@ function buildLandingPayloadFields(sessionId, override) {
691
698
  };
692
699
  }
693
700
 
701
+ // ../tracking-core/src/uuid.ts
702
+ function uuidV4() {
703
+ const cryptoApi = globalThis.crypto;
704
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
705
+ const bytes = new Uint8Array(16);
706
+ if (typeof cryptoApi?.getRandomValues === "function") cryptoApi.getRandomValues(bytes);
707
+ else for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
708
+ bytes[6] = bytes[6] & 15 | 64;
709
+ bytes[8] = bytes[8] & 63 | 128;
710
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
711
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
712
+ }
713
+
694
714
  // ../tracking-core/src/session.ts
695
715
  var VISITOR_STORAGE_KEY = "aranova_tracking_visitor";
696
716
  var SESSION_STORAGE_KEY = "aranova_tracking_session";
697
717
  var SESSION_IDLE_MS = 30 * 60 * 1e3;
698
- function safeUuid() {
699
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
700
- return crypto.randomUUID();
701
- return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
702
- }
703
718
  function readLocalStorage(key) {
704
719
  try {
705
720
  return window.localStorage.getItem(key);
@@ -714,10 +729,10 @@ function writeLocalStorage(key, value) {
714
729
  }
715
730
  }
716
731
  function getVisitorId() {
717
- if (typeof window === "undefined") return safeUuid();
732
+ if (typeof window === "undefined") return uuidV4();
718
733
  const existing = readLocalStorage(VISITOR_STORAGE_KEY);
719
734
  if (existing && existing.length > 0) return existing;
720
- const fresh = safeUuid();
735
+ const fresh = uuidV4();
721
736
  writeLocalStorage(VISITOR_STORAGE_KEY, fresh);
722
737
  return fresh;
723
738
  }
@@ -729,7 +744,7 @@ function adoptVisitorId(visitorId) {
729
744
  return readLocalStorage(VISITOR_STORAGE_KEY) === visitorId;
730
745
  }
731
746
  function getOrRotateSessionId(now = Date.now()) {
732
- if (typeof window === "undefined") return { id: safeUuid(), isNew: true };
747
+ if (typeof window === "undefined") return { id: uuidV4(), isNew: true };
733
748
  const raw = readLocalStorage(SESSION_STORAGE_KEY);
734
749
  if (raw) {
735
750
  try {
@@ -744,7 +759,7 @@ function getOrRotateSessionId(now = Date.now()) {
744
759
  } catch {
745
760
  }
746
761
  }
747
- const fresh = { id: safeUuid(), last_event_at: now };
762
+ const fresh = { id: uuidV4(), last_event_at: now };
748
763
  writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));
749
764
  return { id: fresh.id, isNew: true };
750
765
  }
@@ -977,6 +992,69 @@ function parseFiring(value) {
977
992
  currency: typeof f.currency === "string" ? f.currency : null
978
993
  };
979
994
  }
995
+ function isNumberList(value) {
996
+ return Array.isArray(value) && value.every((v) => typeof v === "number");
997
+ }
998
+ function isStringList(value) {
999
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
1000
+ }
1001
+ function parseWindowsMap(value) {
1002
+ const windowsByKey = {};
1003
+ if (!value || typeof value !== "object") return windowsByKey;
1004
+ for (const [key, windows] of Object.entries(value)) {
1005
+ if (!windows || typeof windows !== "object") continue;
1006
+ const w = windows;
1007
+ windowsByKey[key] = {
1008
+ morning: isNumberList(w.morning) ? w.morning : [],
1009
+ afternoon: isNumberList(w.afternoon) ? w.afternoon : []
1010
+ };
1011
+ }
1012
+ return windowsByKey;
1013
+ }
1014
+ function parseBookingRules(value) {
1015
+ if (!value || typeof value !== "object") return null;
1016
+ const r = value;
1017
+ if (typeof r.min_days_ahead !== "number" || typeof r.max_days_ahead !== "number") return null;
1018
+ if (!r.hours || typeof r.hours !== "object") return null;
1019
+ const hours = parseWindowsMap(r.hours);
1020
+ return {
1021
+ min_days_ahead: r.min_days_ahead,
1022
+ max_days_ahead: r.max_days_ahead,
1023
+ hours,
1024
+ closed_dates: isStringList(r.closed_dates) ? r.closed_dates : [],
1025
+ special_hours: parseWindowsMap(r.special_hours),
1026
+ vehicle_required: r.vehicle_required === true,
1027
+ drop_off_options: isStringList(r.drop_off_options) ? r.drop_off_options : []
1028
+ };
1029
+ }
1030
+ function parseBookingOption(value) {
1031
+ if (!value || typeof value !== "object") return [];
1032
+ const o = value;
1033
+ if (typeof o.key !== "string" || !o.key || typeof o.label !== "string") return [];
1034
+ return [{ key: o.key, label: o.label }];
1035
+ }
1036
+ function parseIntegrations(value) {
1037
+ if (!value || typeof value !== "object") return {};
1038
+ const autoleap = value.autoleap;
1039
+ if (!autoleap || typeof autoleap !== "object") return {};
1040
+ const booking = autoleap.booking;
1041
+ if (!booking || typeof booking !== "object") return {};
1042
+ const b = booking;
1043
+ if (typeof b.fallback_url !== "string" || !b.fallback_url) return {};
1044
+ return {
1045
+ autoleap: {
1046
+ booking: {
1047
+ enabled: b.enabled === true,
1048
+ fallback_url: b.fallback_url,
1049
+ timezone: typeof b.timezone === "string" ? b.timezone : "UTC",
1050
+ config_version: typeof b.config_version === "number" ? b.config_version : null,
1051
+ accepts_bookings: b.accepts_bookings === true,
1052
+ rules: parseBookingRules(b.rules),
1053
+ options: Array.isArray(b.options) ? b.options.flatMap(parseBookingOption) : []
1054
+ }
1055
+ }
1056
+ };
1057
+ }
980
1058
  function parseTrigger(value) {
981
1059
  if (!value || typeof value !== "object") return null;
982
1060
  const t = value;
@@ -1036,7 +1114,8 @@ function parseConversionConfig(raw) {
1036
1114
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
1037
1115
  services,
1038
1116
  goals,
1039
- linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : []
1117
+ linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : [],
1118
+ integrations: parseIntegrations(obj.integrations)
1040
1119
  };
1041
1120
  }
1042
1121
  var CACHE_PREFIX = "_aranova_cfg_";
@@ -1460,6 +1539,13 @@ var TrackingConfigRuntime = class {
1460
1539
  linkedHostnames() {
1461
1540
  return this.current?.linked_hostnames ?? [];
1462
1541
  }
1542
+ /**
1543
+ * Integration blocks, from the cached object even while unconfirmed: the booking fallback
1544
+ * link is safe to show stale. Same reference until the next adopt, for `useSyncExternalStore`.
1545
+ */
1546
+ integrations() {
1547
+ return this.current?.integrations ?? null;
1548
+ }
1463
1549
  async revalidateNow() {
1464
1550
  try {
1465
1551
  const headers = {};
@@ -1629,6 +1715,105 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1629
1715
  return runtime;
1630
1716
  }
1631
1717
 
1718
+ // ../../contracts/autoleap-booking-links.json
1719
+ var autoleap_booking_links_default = {
1720
+ hosts: ["myalp.io"]
1721
+ };
1722
+
1723
+ // ../tracking-core/src/integrations-public.ts
1724
+ var HANDOFF_HOSTS = new Set(autoleap_booking_links_default.hosts);
1725
+ function getBookingIntegration(integrations) {
1726
+ const booking = integrations?.autoleap?.booking;
1727
+ if (!booking) return null;
1728
+ const rules = booking.rules ? camelRules(booking.rules) : null;
1729
+ return {
1730
+ provider: "autoleap",
1731
+ enabled: booking.enabled,
1732
+ fallbackUrl: booking.fallback_url,
1733
+ acceptsBookings: booking.accepts_bookings && rules !== null && booking.config_version !== null,
1734
+ timezone: booking.timezone,
1735
+ configVersion: booking.config_version,
1736
+ rules,
1737
+ options: booking.options.map((option) => ({ key: option.key, label: option.label }))
1738
+ };
1739
+ }
1740
+ function camelRules(rules) {
1741
+ return {
1742
+ minDaysAhead: rules.min_days_ahead,
1743
+ maxDaysAhead: rules.max_days_ahead,
1744
+ hours: rules.hours,
1745
+ closedDates: rules.closed_dates,
1746
+ specialHours: rules.special_hours,
1747
+ vehicleRequired: rules.vehicle_required,
1748
+ dropOffOptions: rules.drop_off_options
1749
+ };
1750
+ }
1751
+ var WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
1752
+ function shopToday(timeZone, now = /* @__PURE__ */ new Date()) {
1753
+ const parts = new Intl.DateTimeFormat("en-CA", {
1754
+ timeZone,
1755
+ year: "numeric",
1756
+ month: "2-digit",
1757
+ day: "2-digit"
1758
+ }).formatToParts(now);
1759
+ const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
1760
+ return `${part("year")}-${part("month")}-${part("day")}`;
1761
+ }
1762
+ function addDays(isoDate, days) {
1763
+ const [year, month, day] = isoDate.split("-").map(Number);
1764
+ const date = new Date(Date.UTC(year, month - 1, day + days));
1765
+ return date.toISOString().slice(0, 10);
1766
+ }
1767
+ function bookableSlots(rules, { timeZone, now = /* @__PURE__ */ new Date() }) {
1768
+ const today = shopToday(timeZone, now);
1769
+ const days = [];
1770
+ const closed = new Set(rules.closedDates);
1771
+ for (let offset = rules.minDaysAhead; offset <= rules.maxDaysAhead; offset += 1) {
1772
+ const date = addDays(today, offset);
1773
+ if (closed.has(date)) continue;
1774
+ const weekday = WEEKDAYS[(/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay()];
1775
+ const windows = rules.specialHours[date] ?? rules.hours[weekday];
1776
+ if (!windows || windows.morning.length === 0 && windows.afternoon.length === 0) continue;
1777
+ days.push({
1778
+ date,
1779
+ weekday,
1780
+ morning: [...windows.morning],
1781
+ afternoon: [...windows.afternoon]
1782
+ });
1783
+ }
1784
+ return days;
1785
+ }
1786
+ function buildBookingHandoffUrl(fallbackUrl, prefill = {}) {
1787
+ let url;
1788
+ try {
1789
+ url = new URL(fallbackUrl);
1790
+ } catch {
1791
+ return null;
1792
+ }
1793
+ if (url.protocol !== "https:" || !HANDOFF_HOSTS.has(url.hostname) || url.username || url.password || url.port) {
1794
+ return null;
1795
+ }
1796
+ const [firstName, lastName] = prefill.firstName?.trim() ? [prefill.firstName, prefill.lastName] : splitFullName(prefill.name);
1797
+ const values = [
1798
+ ["firstName", firstName?.trim()],
1799
+ ["lastName", lastName?.trim()],
1800
+ ["phone", prefill.phone ? nationalDigits(prefill.phone) : ""],
1801
+ ["email", prefill.email?.trim()]
1802
+ ];
1803
+ for (const [key, value] of values) {
1804
+ if (value) url.searchParams.set(key, value);
1805
+ }
1806
+ return url.toString();
1807
+ }
1808
+ function splitFullName(name) {
1809
+ const [first = "", ...rest] = (name ?? "").trim().split(/\s+/);
1810
+ return [first, rest.join(" ")];
1811
+ }
1812
+ function nationalDigits(phone) {
1813
+ const parsed = parsePhone(phone);
1814
+ return (parsed.isValid ? parsed.national : phone).replace(/\D/g, "");
1815
+ }
1816
+
1632
1817
  // ../tracking-core/src/resources/conversion-autofire.ts
1633
1818
  function currentPath() {
1634
1819
  return typeof window === "undefined" ? "" : window.location.pathname;
@@ -3448,6 +3633,9 @@ function attachPhoneClickCapture(client, config) {
3448
3633
  };
3449
3634
  }
3450
3635
 
3636
+ // ../tracking-core/src/resources/sales/bookings.ts
3637
+ var newBookingAttemptKey = uuidV4;
3638
+
3451
3639
  // src/hooks.ts
3452
3640
  function useGclid() {
3453
3641
  const [gclid, setGclid] = (0, import_react.useState)(null);
@@ -3779,7 +3967,7 @@ function GoogleAdsTracking(props) {
3779
3967
  var import_react6 = require("react");
3780
3968
 
3781
3969
  // package.json
3782
- var version = "1.2.5";
3970
+ var version = "1.3.0";
3783
3971
 
3784
3972
  // ../tracking-core/src/phone-react.tsx
3785
3973
  var import_react5 = require("react");
@@ -4046,13 +4234,95 @@ function createTracking(options) {
4046
4234
  )
4047
4235
  };
4048
4236
  }
4237
+
4238
+ // ../tracking-core/src/booking-react.tsx
4239
+ var import_react7 = require("react");
4240
+ var import_jsx_runtime4 = require("react/jsx-runtime");
4241
+ function serverSnapshot() {
4242
+ return null;
4243
+ }
4244
+ function useBookingIntegration(trackingConfig) {
4245
+ registerCapability("booking_handoff");
4246
+ const subscribe = (0, import_react7.useCallback)(
4247
+ (listener) => trackingConfig ? getTrackingConfigRuntime(trackingConfig).subscribe(listener) : () => {
4248
+ },
4249
+ [trackingConfig]
4250
+ );
4251
+ const integrations = (0, import_react7.useSyncExternalStore)(
4252
+ subscribe,
4253
+ () => trackingConfig ? getTrackingConfigRuntime(trackingConfig).integrations() : null,
4254
+ serverSnapshot
4255
+ );
4256
+ return (0, import_react7.useMemo)(() => getBookingIntegration(integrations), [integrations]);
4257
+ }
4258
+ function useBookingHandoffUrl(trackingConfig, { fallbackUrl, prefill } = {}) {
4259
+ const booking = useBookingIntegration(trackingConfig);
4260
+ const base = booking?.fallbackUrl ?? fallbackUrl;
4261
+ return base ? buildBookingHandoffUrl(base, prefill) : null;
4262
+ }
4263
+ function BookingHandoffLink({
4264
+ trackingConfig,
4265
+ fallbackUrl,
4266
+ prefill,
4267
+ children,
4268
+ target = "_blank",
4269
+ rel = "noopener noreferrer",
4270
+ ...anchorProps
4271
+ }) {
4272
+ const href = useBookingHandoffUrl(trackingConfig, { fallbackUrl, prefill });
4273
+ if (!href) return null;
4274
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { ...anchorProps, href, target, rel, children });
4275
+ }
4276
+ function useBookingSlots(trackingConfig) {
4277
+ const booking = useBookingIntegration(trackingConfig);
4278
+ if (!booking?.acceptsBookings || !booking.rules) return [];
4279
+ return bookableSlots(booking.rules, { timeZone: booking.timezone });
4280
+ }
4281
+ function useBookingSubmit(bookings, options = {}) {
4282
+ registerCapability("booking_integration");
4283
+ const [outcome, setOutcome] = (0, import_react7.useState)(null);
4284
+ const [submitting, setSubmitting] = (0, import_react7.useState)(false);
4285
+ const attemptKey = (0, import_react7.useRef)(null);
4286
+ const inFlight = (0, import_react7.useRef)(null);
4287
+ const { formSubmit } = options;
4288
+ const submit = (0, import_react7.useCallback)(
4289
+ (input) => {
4290
+ if (inFlight.current) return inFlight.current;
4291
+ attemptKey.current ?? (attemptKey.current = newBookingAttemptKey());
4292
+ setSubmitting(true);
4293
+ const run = bookings.submit(input, { attemptKey: attemptKey.current, formSubmit }).then((result) => {
4294
+ const unrecorded = result.status === "invalid" || result.status === "stale" || result.status === "failed" && result.id === null;
4295
+ if (!unrecorded) attemptKey.current = null;
4296
+ setOutcome(result);
4297
+ return result;
4298
+ }).finally(() => {
4299
+ inFlight.current = null;
4300
+ setSubmitting(false);
4301
+ });
4302
+ inFlight.current = run;
4303
+ return run;
4304
+ },
4305
+ [bookings, formSubmit]
4306
+ );
4307
+ const reset = (0, import_react7.useCallback)(() => {
4308
+ attemptKey.current = null;
4309
+ setOutcome(null);
4310
+ }, []);
4311
+ const status = submitting ? "submitting" : outcome?.status ?? "idle";
4312
+ return { status, outcome, submit, reset };
4313
+ }
4049
4314
  // Annotate the CommonJS export names for ESM import in node:
4050
4315
  0 && (module.exports = {
4051
4316
  AdPlatformTracking,
4317
+ BookingHandoffLink,
4052
4318
  ConsentBanner,
4053
4319
  GoogleAdsTracking,
4054
4320
  PhoneField,
4055
4321
  createTracking,
4322
+ useBookingHandoffUrl,
4323
+ useBookingIntegration,
4324
+ useBookingSlots,
4325
+ useBookingSubmit,
4056
4326
  useConsent,
4057
4327
  useConsentState,
4058
4328
  useCookiePreferences,