@aranova/tracking-react 1.2.4 → 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.mjs CHANGED
@@ -243,6 +243,8 @@ var TRACKING_CAPABILITIES = [
243
243
  "ad_tags_meta",
244
244
  "base_tracking",
245
245
  "blog_rendering",
246
+ "booking_handoff",
247
+ "booking_integration",
246
248
  "calendar_read",
247
249
  "calendar_write",
248
250
  "consent_controls",
@@ -645,15 +647,23 @@ function buildLandingPayloadFields(sessionId, override) {
645
647
  };
646
648
  }
647
649
 
650
+ // ../tracking-core/src/uuid.ts
651
+ function uuidV4() {
652
+ const cryptoApi = globalThis.crypto;
653
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
654
+ const bytes = new Uint8Array(16);
655
+ if (typeof cryptoApi?.getRandomValues === "function") cryptoApi.getRandomValues(bytes);
656
+ else for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
657
+ bytes[6] = bytes[6] & 15 | 64;
658
+ bytes[8] = bytes[8] & 63 | 128;
659
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
660
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
661
+ }
662
+
648
663
  // ../tracking-core/src/session.ts
649
664
  var VISITOR_STORAGE_KEY = "aranova_tracking_visitor";
650
665
  var SESSION_STORAGE_KEY = "aranova_tracking_session";
651
666
  var SESSION_IDLE_MS = 30 * 60 * 1e3;
652
- function safeUuid() {
653
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
654
- return crypto.randomUUID();
655
- return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
656
- }
657
667
  function readLocalStorage(key) {
658
668
  try {
659
669
  return window.localStorage.getItem(key);
@@ -668,10 +678,10 @@ function writeLocalStorage(key, value) {
668
678
  }
669
679
  }
670
680
  function getVisitorId() {
671
- if (typeof window === "undefined") return safeUuid();
681
+ if (typeof window === "undefined") return uuidV4();
672
682
  const existing = readLocalStorage(VISITOR_STORAGE_KEY);
673
683
  if (existing && existing.length > 0) return existing;
674
- const fresh = safeUuid();
684
+ const fresh = uuidV4();
675
685
  writeLocalStorage(VISITOR_STORAGE_KEY, fresh);
676
686
  return fresh;
677
687
  }
@@ -683,7 +693,7 @@ function adoptVisitorId(visitorId) {
683
693
  return readLocalStorage(VISITOR_STORAGE_KEY) === visitorId;
684
694
  }
685
695
  function getOrRotateSessionId(now = Date.now()) {
686
- if (typeof window === "undefined") return { id: safeUuid(), isNew: true };
696
+ if (typeof window === "undefined") return { id: uuidV4(), isNew: true };
687
697
  const raw = readLocalStorage(SESSION_STORAGE_KEY);
688
698
  if (raw) {
689
699
  try {
@@ -698,7 +708,7 @@ function getOrRotateSessionId(now = Date.now()) {
698
708
  } catch {
699
709
  }
700
710
  }
701
- const fresh = { id: safeUuid(), last_event_at: now };
711
+ const fresh = { id: uuidV4(), last_event_at: now };
702
712
  writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));
703
713
  return { id: fresh.id, isNew: true };
704
714
  }
@@ -931,6 +941,69 @@ function parseFiring(value) {
931
941
  currency: typeof f.currency === "string" ? f.currency : null
932
942
  };
933
943
  }
944
+ function isNumberList(value) {
945
+ return Array.isArray(value) && value.every((v) => typeof v === "number");
946
+ }
947
+ function isStringList(value) {
948
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
949
+ }
950
+ function parseWindowsMap(value) {
951
+ const windowsByKey = {};
952
+ if (!value || typeof value !== "object") return windowsByKey;
953
+ for (const [key, windows] of Object.entries(value)) {
954
+ if (!windows || typeof windows !== "object") continue;
955
+ const w = windows;
956
+ windowsByKey[key] = {
957
+ morning: isNumberList(w.morning) ? w.morning : [],
958
+ afternoon: isNumberList(w.afternoon) ? w.afternoon : []
959
+ };
960
+ }
961
+ return windowsByKey;
962
+ }
963
+ function parseBookingRules(value) {
964
+ if (!value || typeof value !== "object") return null;
965
+ const r = value;
966
+ if (typeof r.min_days_ahead !== "number" || typeof r.max_days_ahead !== "number") return null;
967
+ if (!r.hours || typeof r.hours !== "object") return null;
968
+ const hours = parseWindowsMap(r.hours);
969
+ return {
970
+ min_days_ahead: r.min_days_ahead,
971
+ max_days_ahead: r.max_days_ahead,
972
+ hours,
973
+ closed_dates: isStringList(r.closed_dates) ? r.closed_dates : [],
974
+ special_hours: parseWindowsMap(r.special_hours),
975
+ vehicle_required: r.vehicle_required === true,
976
+ drop_off_options: isStringList(r.drop_off_options) ? r.drop_off_options : []
977
+ };
978
+ }
979
+ function parseBookingOption(value) {
980
+ if (!value || typeof value !== "object") return [];
981
+ const o = value;
982
+ if (typeof o.key !== "string" || !o.key || typeof o.label !== "string") return [];
983
+ return [{ key: o.key, label: o.label }];
984
+ }
985
+ function parseIntegrations(value) {
986
+ if (!value || typeof value !== "object") return {};
987
+ const autoleap = value.autoleap;
988
+ if (!autoleap || typeof autoleap !== "object") return {};
989
+ const booking = autoleap.booking;
990
+ if (!booking || typeof booking !== "object") return {};
991
+ const b = booking;
992
+ if (typeof b.fallback_url !== "string" || !b.fallback_url) return {};
993
+ return {
994
+ autoleap: {
995
+ booking: {
996
+ enabled: b.enabled === true,
997
+ fallback_url: b.fallback_url,
998
+ timezone: typeof b.timezone === "string" ? b.timezone : "UTC",
999
+ config_version: typeof b.config_version === "number" ? b.config_version : null,
1000
+ accepts_bookings: b.accepts_bookings === true,
1001
+ rules: parseBookingRules(b.rules),
1002
+ options: Array.isArray(b.options) ? b.options.flatMap(parseBookingOption) : []
1003
+ }
1004
+ }
1005
+ };
1006
+ }
934
1007
  function parseTrigger(value) {
935
1008
  if (!value || typeof value !== "object") return null;
936
1009
  const t = value;
@@ -990,7 +1063,8 @@ function parseConversionConfig(raw) {
990
1063
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
991
1064
  services,
992
1065
  goals,
993
- linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : []
1066
+ linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : [],
1067
+ integrations: parseIntegrations(obj.integrations)
994
1068
  };
995
1069
  }
996
1070
  var CACHE_PREFIX = "_aranova_cfg_";
@@ -1414,6 +1488,13 @@ var TrackingConfigRuntime = class {
1414
1488
  linkedHostnames() {
1415
1489
  return this.current?.linked_hostnames ?? [];
1416
1490
  }
1491
+ /**
1492
+ * Integration blocks, from the cached object even while unconfirmed: the booking fallback
1493
+ * link is safe to show stale. Same reference until the next adopt, for `useSyncExternalStore`.
1494
+ */
1495
+ integrations() {
1496
+ return this.current?.integrations ?? null;
1497
+ }
1417
1498
  async revalidateNow() {
1418
1499
  try {
1419
1500
  const headers = {};
@@ -1583,6 +1664,105 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1583
1664
  return runtime;
1584
1665
  }
1585
1666
 
1667
+ // ../../contracts/autoleap-booking-links.json
1668
+ var autoleap_booking_links_default = {
1669
+ hosts: ["myalp.io"]
1670
+ };
1671
+
1672
+ // ../tracking-core/src/integrations-public.ts
1673
+ var HANDOFF_HOSTS = new Set(autoleap_booking_links_default.hosts);
1674
+ function getBookingIntegration(integrations) {
1675
+ const booking = integrations?.autoleap?.booking;
1676
+ if (!booking) return null;
1677
+ const rules = booking.rules ? camelRules(booking.rules) : null;
1678
+ return {
1679
+ provider: "autoleap",
1680
+ enabled: booking.enabled,
1681
+ fallbackUrl: booking.fallback_url,
1682
+ acceptsBookings: booking.accepts_bookings && rules !== null && booking.config_version !== null,
1683
+ timezone: booking.timezone,
1684
+ configVersion: booking.config_version,
1685
+ rules,
1686
+ options: booking.options.map((option) => ({ key: option.key, label: option.label }))
1687
+ };
1688
+ }
1689
+ function camelRules(rules) {
1690
+ return {
1691
+ minDaysAhead: rules.min_days_ahead,
1692
+ maxDaysAhead: rules.max_days_ahead,
1693
+ hours: rules.hours,
1694
+ closedDates: rules.closed_dates,
1695
+ specialHours: rules.special_hours,
1696
+ vehicleRequired: rules.vehicle_required,
1697
+ dropOffOptions: rules.drop_off_options
1698
+ };
1699
+ }
1700
+ var WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
1701
+ function shopToday(timeZone, now = /* @__PURE__ */ new Date()) {
1702
+ const parts = new Intl.DateTimeFormat("en-CA", {
1703
+ timeZone,
1704
+ year: "numeric",
1705
+ month: "2-digit",
1706
+ day: "2-digit"
1707
+ }).formatToParts(now);
1708
+ const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
1709
+ return `${part("year")}-${part("month")}-${part("day")}`;
1710
+ }
1711
+ function addDays(isoDate, days) {
1712
+ const [year, month, day] = isoDate.split("-").map(Number);
1713
+ const date = new Date(Date.UTC(year, month - 1, day + days));
1714
+ return date.toISOString().slice(0, 10);
1715
+ }
1716
+ function bookableSlots(rules, { timeZone, now = /* @__PURE__ */ new Date() }) {
1717
+ const today = shopToday(timeZone, now);
1718
+ const days = [];
1719
+ const closed = new Set(rules.closedDates);
1720
+ for (let offset = rules.minDaysAhead; offset <= rules.maxDaysAhead; offset += 1) {
1721
+ const date = addDays(today, offset);
1722
+ if (closed.has(date)) continue;
1723
+ const weekday = WEEKDAYS[(/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay()];
1724
+ const windows = rules.specialHours[date] ?? rules.hours[weekday];
1725
+ if (!windows || windows.morning.length === 0 && windows.afternoon.length === 0) continue;
1726
+ days.push({
1727
+ date,
1728
+ weekday,
1729
+ morning: [...windows.morning],
1730
+ afternoon: [...windows.afternoon]
1731
+ });
1732
+ }
1733
+ return days;
1734
+ }
1735
+ function buildBookingHandoffUrl(fallbackUrl, prefill = {}) {
1736
+ let url;
1737
+ try {
1738
+ url = new URL(fallbackUrl);
1739
+ } catch {
1740
+ return null;
1741
+ }
1742
+ if (url.protocol !== "https:" || !HANDOFF_HOSTS.has(url.hostname) || url.username || url.password || url.port) {
1743
+ return null;
1744
+ }
1745
+ const [firstName, lastName] = prefill.firstName?.trim() ? [prefill.firstName, prefill.lastName] : splitFullName(prefill.name);
1746
+ const values = [
1747
+ ["firstName", firstName?.trim()],
1748
+ ["lastName", lastName?.trim()],
1749
+ ["phone", prefill.phone ? nationalDigits(prefill.phone) : ""],
1750
+ ["email", prefill.email?.trim()]
1751
+ ];
1752
+ for (const [key, value] of values) {
1753
+ if (value) url.searchParams.set(key, value);
1754
+ }
1755
+ return url.toString();
1756
+ }
1757
+ function splitFullName(name) {
1758
+ const [first = "", ...rest] = (name ?? "").trim().split(/\s+/);
1759
+ return [first, rest.join(" ")];
1760
+ }
1761
+ function nationalDigits(phone) {
1762
+ const parsed = parsePhone(phone);
1763
+ return (parsed.isValid ? parsed.national : phone).replace(/\D/g, "");
1764
+ }
1765
+
1586
1766
  // ../tracking-core/src/resources/conversion-autofire.ts
1587
1767
  function currentPath() {
1588
1768
  return typeof window === "undefined" ? "" : window.location.pathname;
@@ -3402,6 +3582,9 @@ function attachPhoneClickCapture(client, config) {
3402
3582
  };
3403
3583
  }
3404
3584
 
3585
+ // ../tracking-core/src/resources/sales/bookings.ts
3586
+ var newBookingAttemptKey = uuidV4;
3587
+
3405
3588
  // src/hooks.ts
3406
3589
  function useGclid() {
3407
3590
  const [gclid, setGclid] = useState(null);
@@ -3733,7 +3916,7 @@ function GoogleAdsTracking(props) {
3733
3916
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
3734
3917
 
3735
3918
  // package.json
3736
- var version = "1.2.4";
3919
+ var version = "1.3.0";
3737
3920
 
3738
3921
  // ../tracking-core/src/phone-react.tsx
3739
3922
  import {
@@ -4007,12 +4190,94 @@ function createTracking(options) {
4007
4190
  )
4008
4191
  };
4009
4192
  }
4193
+
4194
+ // ../tracking-core/src/booking-react.tsx
4195
+ import { useCallback as useCallback3, useMemo as useMemo5, useRef, useState as useState4, useSyncExternalStore } from "react";
4196
+ import { jsx as jsx4 } from "react/jsx-runtime";
4197
+ function serverSnapshot() {
4198
+ return null;
4199
+ }
4200
+ function useBookingIntegration(trackingConfig) {
4201
+ registerCapability("booking_handoff");
4202
+ const subscribe = useCallback3(
4203
+ (listener) => trackingConfig ? getTrackingConfigRuntime(trackingConfig).subscribe(listener) : () => {
4204
+ },
4205
+ [trackingConfig]
4206
+ );
4207
+ const integrations = useSyncExternalStore(
4208
+ subscribe,
4209
+ () => trackingConfig ? getTrackingConfigRuntime(trackingConfig).integrations() : null,
4210
+ serverSnapshot
4211
+ );
4212
+ return useMemo5(() => getBookingIntegration(integrations), [integrations]);
4213
+ }
4214
+ function useBookingHandoffUrl(trackingConfig, { fallbackUrl, prefill } = {}) {
4215
+ const booking = useBookingIntegration(trackingConfig);
4216
+ const base = booking?.fallbackUrl ?? fallbackUrl;
4217
+ return base ? buildBookingHandoffUrl(base, prefill) : null;
4218
+ }
4219
+ function BookingHandoffLink({
4220
+ trackingConfig,
4221
+ fallbackUrl,
4222
+ prefill,
4223
+ children,
4224
+ target = "_blank",
4225
+ rel = "noopener noreferrer",
4226
+ ...anchorProps
4227
+ }) {
4228
+ const href = useBookingHandoffUrl(trackingConfig, { fallbackUrl, prefill });
4229
+ if (!href) return null;
4230
+ return /* @__PURE__ */ jsx4("a", { ...anchorProps, href, target, rel, children });
4231
+ }
4232
+ function useBookingSlots(trackingConfig) {
4233
+ const booking = useBookingIntegration(trackingConfig);
4234
+ if (!booking?.acceptsBookings || !booking.rules) return [];
4235
+ return bookableSlots(booking.rules, { timeZone: booking.timezone });
4236
+ }
4237
+ function useBookingSubmit(bookings, options = {}) {
4238
+ registerCapability("booking_integration");
4239
+ const [outcome, setOutcome] = useState4(null);
4240
+ const [submitting, setSubmitting] = useState4(false);
4241
+ const attemptKey = useRef(null);
4242
+ const inFlight = useRef(null);
4243
+ const { formSubmit } = options;
4244
+ const submit = useCallback3(
4245
+ (input) => {
4246
+ if (inFlight.current) return inFlight.current;
4247
+ attemptKey.current ?? (attemptKey.current = newBookingAttemptKey());
4248
+ setSubmitting(true);
4249
+ const run = bookings.submit(input, { attemptKey: attemptKey.current, formSubmit }).then((result) => {
4250
+ const unrecorded = result.status === "invalid" || result.status === "stale" || result.status === "failed" && result.id === null;
4251
+ if (!unrecorded) attemptKey.current = null;
4252
+ setOutcome(result);
4253
+ return result;
4254
+ }).finally(() => {
4255
+ inFlight.current = null;
4256
+ setSubmitting(false);
4257
+ });
4258
+ inFlight.current = run;
4259
+ return run;
4260
+ },
4261
+ [bookings, formSubmit]
4262
+ );
4263
+ const reset = useCallback3(() => {
4264
+ attemptKey.current = null;
4265
+ setOutcome(null);
4266
+ }, []);
4267
+ const status = submitting ? "submitting" : outcome?.status ?? "idle";
4268
+ return { status, outcome, submit, reset };
4269
+ }
4010
4270
  export {
4011
4271
  AdPlatformTracking,
4272
+ BookingHandoffLink,
4012
4273
  ConsentBanner,
4013
4274
  GoogleAdsTracking,
4014
4275
  PhoneField,
4015
4276
  createTracking,
4277
+ useBookingHandoffUrl,
4278
+ useBookingIntegration,
4279
+ useBookingSlots,
4280
+ useBookingSubmit,
4016
4281
  useConsent,
4017
4282
  useConsentState,
4018
4283
  useCookiePreferences,