@aranova/tracking-react 1.2.5 → 1.4.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
  }
@@ -964,6 +979,10 @@ function fireConversionWithConsent(input, options) {
964
979
  }
965
980
 
966
981
  // ../tracking-core/src/resources/conversion-config.ts
982
+ var BOOKING_MODES = ["live", "test", "simulated"];
983
+ function isBookingMode(value) {
984
+ return BOOKING_MODES.includes(value);
985
+ }
967
986
  function isStringMap(value) {
968
987
  return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
969
988
  }
@@ -977,6 +996,70 @@ function parseFiring(value) {
977
996
  currency: typeof f.currency === "string" ? f.currency : null
978
997
  };
979
998
  }
999
+ function isNumberList(value) {
1000
+ return Array.isArray(value) && value.every((v) => typeof v === "number");
1001
+ }
1002
+ function isStringList(value) {
1003
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
1004
+ }
1005
+ function parseWindowsMap(value) {
1006
+ const windowsByKey = {};
1007
+ if (!value || typeof value !== "object") return windowsByKey;
1008
+ for (const [key, windows] of Object.entries(value)) {
1009
+ if (!windows || typeof windows !== "object") continue;
1010
+ const w = windows;
1011
+ windowsByKey[key] = {
1012
+ morning: isNumberList(w.morning) ? w.morning : [],
1013
+ afternoon: isNumberList(w.afternoon) ? w.afternoon : []
1014
+ };
1015
+ }
1016
+ return windowsByKey;
1017
+ }
1018
+ function parseBookingRules(value) {
1019
+ if (!value || typeof value !== "object") return null;
1020
+ const r = value;
1021
+ if (typeof r.min_days_ahead !== "number" || typeof r.max_days_ahead !== "number") return null;
1022
+ if (!r.hours || typeof r.hours !== "object") return null;
1023
+ const hours = parseWindowsMap(r.hours);
1024
+ return {
1025
+ min_days_ahead: r.min_days_ahead,
1026
+ max_days_ahead: r.max_days_ahead,
1027
+ hours,
1028
+ closed_dates: isStringList(r.closed_dates) ? r.closed_dates : [],
1029
+ special_hours: parseWindowsMap(r.special_hours),
1030
+ vehicle_required: r.vehicle_required === true,
1031
+ drop_off_options: isStringList(r.drop_off_options) ? r.drop_off_options : []
1032
+ };
1033
+ }
1034
+ function parseBookingOption(value) {
1035
+ if (!value || typeof value !== "object") return [];
1036
+ const o = value;
1037
+ if (typeof o.key !== "string" || !o.key || typeof o.label !== "string") return [];
1038
+ return [{ key: o.key, label: o.label }];
1039
+ }
1040
+ function parseIntegrations(value) {
1041
+ if (!value || typeof value !== "object") return {};
1042
+ const autoleap = value.autoleap;
1043
+ if (!autoleap || typeof autoleap !== "object") return {};
1044
+ const booking = autoleap.booking;
1045
+ if (!booking || typeof booking !== "object") return {};
1046
+ const b = booking;
1047
+ if (typeof b.fallback_url !== "string" || !b.fallback_url) return {};
1048
+ return {
1049
+ autoleap: {
1050
+ booking: {
1051
+ enabled: b.enabled === true,
1052
+ fallback_url: b.fallback_url,
1053
+ timezone: typeof b.timezone === "string" ? b.timezone : "UTC",
1054
+ config_version: typeof b.config_version === "number" ? b.config_version : null,
1055
+ accepts_bookings: b.accepts_bookings === true,
1056
+ rules: parseBookingRules(b.rules),
1057
+ options: Array.isArray(b.options) ? b.options.flatMap(parseBookingOption) : [],
1058
+ ...isBookingMode(b.mode) ? { mode: b.mode } : {}
1059
+ }
1060
+ }
1061
+ };
1062
+ }
980
1063
  function parseTrigger(value) {
981
1064
  if (!value || typeof value !== "object") return null;
982
1065
  const t = value;
@@ -1036,7 +1119,8 @@ function parseConversionConfig(raw) {
1036
1119
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
1037
1120
  services,
1038
1121
  goals,
1039
- linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : []
1122
+ linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : [],
1123
+ integrations: parseIntegrations(obj.integrations)
1040
1124
  };
1041
1125
  }
1042
1126
  var CACHE_PREFIX = "_aranova_cfg_";
@@ -1460,6 +1544,13 @@ var TrackingConfigRuntime = class {
1460
1544
  linkedHostnames() {
1461
1545
  return this.current?.linked_hostnames ?? [];
1462
1546
  }
1547
+ /**
1548
+ * Integration blocks, from the cached object even while unconfirmed: the booking fallback
1549
+ * link is safe to show stale. Same reference until the next adopt, for `useSyncExternalStore`.
1550
+ */
1551
+ integrations() {
1552
+ return this.current?.integrations ?? null;
1553
+ }
1463
1554
  async revalidateNow() {
1464
1555
  try {
1465
1556
  const headers = {};
@@ -1629,6 +1720,106 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1629
1720
  return runtime;
1630
1721
  }
1631
1722
 
1723
+ // ../../contracts/autoleap-booking-links.json
1724
+ var autoleap_booking_links_default = {
1725
+ hosts: ["myalp.io"]
1726
+ };
1727
+
1728
+ // ../tracking-core/src/integrations-public.ts
1729
+ var HANDOFF_HOSTS = new Set(autoleap_booking_links_default.hosts);
1730
+ function getBookingIntegration(integrations) {
1731
+ const booking = integrations?.autoleap?.booking;
1732
+ if (!booking) return null;
1733
+ const rules = booking.rules ? camelRules(booking.rules) : null;
1734
+ return {
1735
+ provider: "autoleap",
1736
+ enabled: booking.enabled,
1737
+ fallbackUrl: booking.fallback_url,
1738
+ acceptsBookings: booking.accepts_bookings && rules !== null && booking.config_version !== null,
1739
+ timezone: booking.timezone,
1740
+ configVersion: booking.config_version,
1741
+ rules,
1742
+ options: booking.options.map((option) => ({ key: option.key, label: option.label })),
1743
+ mode: booking.mode ?? "live"
1744
+ };
1745
+ }
1746
+ function camelRules(rules) {
1747
+ return {
1748
+ minDaysAhead: rules.min_days_ahead,
1749
+ maxDaysAhead: rules.max_days_ahead,
1750
+ hours: rules.hours,
1751
+ closedDates: rules.closed_dates,
1752
+ specialHours: rules.special_hours,
1753
+ vehicleRequired: rules.vehicle_required,
1754
+ dropOffOptions: rules.drop_off_options
1755
+ };
1756
+ }
1757
+ var WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
1758
+ function shopToday(timeZone, now = /* @__PURE__ */ new Date()) {
1759
+ const parts = new Intl.DateTimeFormat("en-CA", {
1760
+ timeZone,
1761
+ year: "numeric",
1762
+ month: "2-digit",
1763
+ day: "2-digit"
1764
+ }).formatToParts(now);
1765
+ const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
1766
+ return `${part("year")}-${part("month")}-${part("day")}`;
1767
+ }
1768
+ function addDays(isoDate, days) {
1769
+ const [year, month, day] = isoDate.split("-").map(Number);
1770
+ const date = new Date(Date.UTC(year, month - 1, day + days));
1771
+ return date.toISOString().slice(0, 10);
1772
+ }
1773
+ function bookableSlots(rules, { timeZone, now = /* @__PURE__ */ new Date() }) {
1774
+ const today = shopToday(timeZone, now);
1775
+ const days = [];
1776
+ const closed = new Set(rules.closedDates);
1777
+ for (let offset = rules.minDaysAhead; offset <= rules.maxDaysAhead; offset += 1) {
1778
+ const date = addDays(today, offset);
1779
+ if (closed.has(date)) continue;
1780
+ const weekday = WEEKDAYS[(/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay()];
1781
+ const windows = rules.specialHours[date] ?? rules.hours[weekday];
1782
+ if (!windows || windows.morning.length === 0 && windows.afternoon.length === 0) continue;
1783
+ days.push({
1784
+ date,
1785
+ weekday,
1786
+ morning: [...windows.morning],
1787
+ afternoon: [...windows.afternoon]
1788
+ });
1789
+ }
1790
+ return days;
1791
+ }
1792
+ function buildBookingHandoffUrl(fallbackUrl, prefill = {}) {
1793
+ let url;
1794
+ try {
1795
+ url = new URL(fallbackUrl);
1796
+ } catch {
1797
+ return null;
1798
+ }
1799
+ if (url.protocol !== "https:" || !HANDOFF_HOSTS.has(url.hostname) || url.username || url.password || url.port) {
1800
+ return null;
1801
+ }
1802
+ const [firstName, lastName] = prefill.firstName?.trim() ? [prefill.firstName, prefill.lastName] : splitFullName(prefill.name);
1803
+ const values = [
1804
+ ["firstName", firstName?.trim()],
1805
+ ["lastName", lastName?.trim()],
1806
+ ["phone", prefill.phone ? nationalDigits(prefill.phone) : ""],
1807
+ ["email", prefill.email?.trim()]
1808
+ ];
1809
+ for (const [key, value] of values) {
1810
+ if (value) url.searchParams.set(key, value);
1811
+ }
1812
+ return url.toString();
1813
+ }
1814
+ function splitFullName(name) {
1815
+ const [first = "", ...rest] = (name ?? "").trim().split(/\s+/);
1816
+ return [first, rest.join(" ")];
1817
+ }
1818
+ function nationalDigits(phone) {
1819
+ const parsed = parsePhone(phone);
1820
+ return (parsed.isValid ? parsed.national : phone).replace(/\D/g, "");
1821
+ }
1822
+
1632
1823
  // ../tracking-core/src/resources/conversion-autofire.ts
1633
1824
  function currentPath() {
1634
1825
  return typeof window === "undefined" ? "" : window.location.pathname;
@@ -3448,6 +3639,9 @@ function attachPhoneClickCapture(client, config) {
3448
3639
  };
3449
3640
  }
3450
3641
 
3642
+ // ../tracking-core/src/resources/sales/bookings.ts
3643
+ var newBookingAttemptKey = uuidV4;
3644
+
3451
3645
  // src/hooks.ts
3452
3646
  function useGclid() {
3453
3647
  const [gclid, setGclid] = (0, import_react.useState)(null);
@@ -3779,7 +3973,7 @@ function GoogleAdsTracking(props) {
3779
3973
  var import_react6 = require("react");
3780
3974
 
3781
3975
  // package.json
3782
- var version = "1.2.5";
3976
+ var version = "1.4.0";
3783
3977
 
3784
3978
  // ../tracking-core/src/phone-react.tsx
3785
3979
  var import_react5 = require("react");
@@ -4046,13 +4240,95 @@ function createTracking(options) {
4046
4240
  )
4047
4241
  };
4048
4242
  }
4243
+
4244
+ // ../tracking-core/src/booking-react.tsx
4245
+ var import_react7 = require("react");
4246
+ var import_jsx_runtime4 = require("react/jsx-runtime");
4247
+ function serverSnapshot() {
4248
+ return null;
4249
+ }
4250
+ function useBookingIntegration(trackingConfig) {
4251
+ registerCapability("booking_handoff");
4252
+ const subscribe = (0, import_react7.useCallback)(
4253
+ (listener) => trackingConfig ? getTrackingConfigRuntime(trackingConfig).subscribe(listener) : () => {
4254
+ },
4255
+ [trackingConfig]
4256
+ );
4257
+ const integrations = (0, import_react7.useSyncExternalStore)(
4258
+ subscribe,
4259
+ () => trackingConfig ? getTrackingConfigRuntime(trackingConfig).integrations() : null,
4260
+ serverSnapshot
4261
+ );
4262
+ return (0, import_react7.useMemo)(() => getBookingIntegration(integrations), [integrations]);
4263
+ }
4264
+ function useBookingHandoffUrl(trackingConfig, { fallbackUrl, prefill } = {}) {
4265
+ const booking = useBookingIntegration(trackingConfig);
4266
+ const base = booking?.fallbackUrl ?? fallbackUrl;
4267
+ return base ? buildBookingHandoffUrl(base, prefill) : null;
4268
+ }
4269
+ function BookingHandoffLink({
4270
+ trackingConfig,
4271
+ fallbackUrl,
4272
+ prefill,
4273
+ children,
4274
+ target = "_blank",
4275
+ rel = "noopener noreferrer",
4276
+ ...anchorProps
4277
+ }) {
4278
+ const href = useBookingHandoffUrl(trackingConfig, { fallbackUrl, prefill });
4279
+ if (!href) return null;
4280
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { ...anchorProps, href, target, rel, children });
4281
+ }
4282
+ function useBookingSlots(trackingConfig) {
4283
+ const booking = useBookingIntegration(trackingConfig);
4284
+ if (!booking?.acceptsBookings || !booking.rules) return [];
4285
+ return bookableSlots(booking.rules, { timeZone: booking.timezone });
4286
+ }
4287
+ function useBookingSubmit(bookings, options = {}) {
4288
+ registerCapability("booking_integration");
4289
+ const [outcome, setOutcome] = (0, import_react7.useState)(null);
4290
+ const [submitting, setSubmitting] = (0, import_react7.useState)(false);
4291
+ const attemptKey = (0, import_react7.useRef)(null);
4292
+ const inFlight = (0, import_react7.useRef)(null);
4293
+ const { formSubmit, simulate } = options;
4294
+ const submit = (0, import_react7.useCallback)(
4295
+ (input) => {
4296
+ if (inFlight.current) return inFlight.current;
4297
+ attemptKey.current ?? (attemptKey.current = newBookingAttemptKey());
4298
+ setSubmitting(true);
4299
+ const run = bookings.submit(input, { attemptKey: attemptKey.current, formSubmit, simulate }).then((result) => {
4300
+ const unrecorded = result.status === "invalid" || result.status === "stale" || result.status === "failed" && result.id === null;
4301
+ if (!unrecorded) attemptKey.current = null;
4302
+ setOutcome(result);
4303
+ return result;
4304
+ }).finally(() => {
4305
+ inFlight.current = null;
4306
+ setSubmitting(false);
4307
+ });
4308
+ inFlight.current = run;
4309
+ return run;
4310
+ },
4311
+ [bookings, formSubmit, simulate]
4312
+ );
4313
+ const reset = (0, import_react7.useCallback)(() => {
4314
+ attemptKey.current = null;
4315
+ setOutcome(null);
4316
+ }, []);
4317
+ const status = submitting ? "submitting" : outcome?.status ?? "idle";
4318
+ return { status, outcome, submit, reset };
4319
+ }
4049
4320
  // Annotate the CommonJS export names for ESM import in node:
4050
4321
  0 && (module.exports = {
4051
4322
  AdPlatformTracking,
4323
+ BookingHandoffLink,
4052
4324
  ConsentBanner,
4053
4325
  GoogleAdsTracking,
4054
4326
  PhoneField,
4055
4327
  createTracking,
4328
+ useBookingHandoffUrl,
4329
+ useBookingIntegration,
4330
+ useBookingSlots,
4331
+ useBookingSubmit,
4056
4332
  useConsent,
4057
4333
  useConsentState,
4058
4334
  useCookiePreferences,