@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/index.js CHANGED
@@ -32,6 +32,7 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  AdPlatformTracking: () => import_client.AdPlatformTracking,
34
34
  AranovaApiError: () => AranovaApiError,
35
+ BookingHandoffLink: () => import_client3.BookingHandoffLink,
35
36
  ConsentBanner: () => import_client.ConsentBanner,
36
37
  DEFAULT_DECLINE_TTL_DAYS: () => DEFAULT_DECLINE_TTL_DAYS,
37
38
  DEFAULT_PHONE_COUNTRY: () => DEFAULT_PHONE_COUNTRY,
@@ -39,9 +40,12 @@ __export(src_exports, {
39
40
  GoogleAdsTracking: () => import_client.GoogleAdsTracking,
40
41
  NAMED_RANGES: () => NAMED_RANGES,
41
42
  PhoneField: () => import_client2.PhoneField,
43
+ SALE_SOURCES: () => SALE_SOURCES,
42
44
  SUPPORTED_CURRENCIES: () => SUPPORTED_CURRENCIES,
43
45
  TRACKING_PARAM_KEYS: () => TRACKING_PARAM_KEYS,
44
46
  TRACKING_RANGES: () => TRACKING_RANGES,
47
+ bookableSlots: () => bookableSlots,
48
+ buildBookingHandoffUrl: () => buildBookingHandoffUrl,
45
49
  captureTrackingParamsFromLocation: () => captureTrackingParamsFromLocation,
46
50
  clearStashedUserData: () => clearStashedUserData,
47
51
  createSalesClient: () => createSalesClient,
@@ -56,10 +60,12 @@ __export(src_exports, {
56
60
  formatPhone: () => formatPhone,
57
61
  formatPhoneAsTyped: () => formatPhoneAsTyped,
58
62
  fromMinor: () => fromMinor,
63
+ getBookingIntegration: () => getBookingIntegration,
59
64
  getConsentChoice: () => getConsentChoice,
60
65
  getConsentState: () => getConsentState,
61
66
  getEventSemantics: () => getEventSemantics,
62
67
  getTrackingConfigRuntime: () => getTrackingConfigRuntime,
68
+ newBookingAttemptKey: () => newBookingAttemptKey,
63
69
  onConsentChange: () => onConsentChange,
64
70
  optIn: () => optIn,
65
71
  optOut: () => optOut,
@@ -74,9 +80,14 @@ __export(src_exports, {
74
80
  saleUpdateSchema: () => saleUpdateSchema,
75
81
  salesRequest: () => salesRequest,
76
82
  setConsentState: () => setConsentState,
83
+ shopToday: () => shopToday,
77
84
  stashUserData: () => stashUserData,
78
85
  toE164: () => toE164,
79
86
  toMinor: () => toMinor,
87
+ useBookingHandoffUrl: () => import_client3.useBookingHandoffUrl,
88
+ useBookingIntegration: () => import_client3.useBookingIntegration,
89
+ useBookingSlots: () => import_client3.useBookingSlots,
90
+ useBookingSubmit: () => import_client3.useBookingSubmit,
80
91
  useConsent: () => import_client.useConsent,
81
92
  useConsentState: () => import_client.useConsentState,
82
93
  useCookiePreferences: () => import_client.useCookiePreferences,
@@ -298,6 +309,8 @@ var TRACKING_CAPABILITIES = [
298
309
  "ad_tags_meta",
299
310
  "base_tracking",
300
311
  "blog_rendering",
312
+ "booking_handoff",
313
+ "booking_integration",
301
314
  "calendar_read",
302
315
  "calendar_write",
303
316
  "consent_controls",
@@ -577,6 +590,42 @@ function buildLandingPayloadFields(sessionId, override) {
577
590
  };
578
591
  }
579
592
 
593
+ // ../tracking-core/src/uuid.ts
594
+ function uuidV4() {
595
+ const cryptoApi = globalThis.crypto;
596
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
597
+ const bytes = new Uint8Array(16);
598
+ if (typeof cryptoApi?.getRandomValues === "function") cryptoApi.getRandomValues(bytes);
599
+ else for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
600
+ bytes[6] = bytes[6] & 15 | 64;
601
+ bytes[8] = bytes[8] & 63 | 128;
602
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
603
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
604
+ }
605
+
606
+ // ../tracking-core/src/session.ts
607
+ var SESSION_STORAGE_KEY = "aranova_tracking_session";
608
+ var SESSION_IDLE_MS = 30 * 60 * 1e3;
609
+ function readLocalStorage(key) {
610
+ try {
611
+ return window.localStorage.getItem(key);
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+ function peekSessionId(now = Date.now()) {
617
+ if (typeof window === "undefined") return null;
618
+ const raw = readLocalStorage(SESSION_STORAGE_KEY);
619
+ if (!raw) return null;
620
+ try {
621
+ const parsed = JSON.parse(raw);
622
+ if (typeof parsed.id !== "string" || typeof parsed.last_event_at !== "number") return null;
623
+ return now - parsed.last_event_at <= SESSION_IDLE_MS ? parsed.id : null;
624
+ } catch {
625
+ return null;
626
+ }
627
+ }
628
+
580
629
  // ../tracking-core/src/cross-site-link.ts
581
630
  var LINK_PARAM = "_arv";
582
631
  var LINK_TOKEN_TTL_MS = 5 * 60 * 1e3;
@@ -705,6 +754,10 @@ function fireConversionWithConsent(input, options) {
705
754
  }
706
755
 
707
756
  // ../tracking-core/src/resources/conversion-config.ts
757
+ var BOOKING_MODES = ["live", "test", "simulated"];
758
+ function isBookingMode(value) {
759
+ return BOOKING_MODES.includes(value);
760
+ }
708
761
  function isStringMap(value) {
709
762
  return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
710
763
  }
@@ -718,6 +771,70 @@ function parseFiring(value) {
718
771
  currency: typeof f.currency === "string" ? f.currency : null
719
772
  };
720
773
  }
774
+ function isNumberList(value) {
775
+ return Array.isArray(value) && value.every((v) => typeof v === "number");
776
+ }
777
+ function isStringList(value) {
778
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
779
+ }
780
+ function parseWindowsMap(value) {
781
+ const windowsByKey = {};
782
+ if (!value || typeof value !== "object") return windowsByKey;
783
+ for (const [key, windows] of Object.entries(value)) {
784
+ if (!windows || typeof windows !== "object") continue;
785
+ const w = windows;
786
+ windowsByKey[key] = {
787
+ morning: isNumberList(w.morning) ? w.morning : [],
788
+ afternoon: isNumberList(w.afternoon) ? w.afternoon : []
789
+ };
790
+ }
791
+ return windowsByKey;
792
+ }
793
+ function parseBookingRules(value) {
794
+ if (!value || typeof value !== "object") return null;
795
+ const r = value;
796
+ if (typeof r.min_days_ahead !== "number" || typeof r.max_days_ahead !== "number") return null;
797
+ if (!r.hours || typeof r.hours !== "object") return null;
798
+ const hours = parseWindowsMap(r.hours);
799
+ return {
800
+ min_days_ahead: r.min_days_ahead,
801
+ max_days_ahead: r.max_days_ahead,
802
+ hours,
803
+ closed_dates: isStringList(r.closed_dates) ? r.closed_dates : [],
804
+ special_hours: parseWindowsMap(r.special_hours),
805
+ vehicle_required: r.vehicle_required === true,
806
+ drop_off_options: isStringList(r.drop_off_options) ? r.drop_off_options : []
807
+ };
808
+ }
809
+ function parseBookingOption(value) {
810
+ if (!value || typeof value !== "object") return [];
811
+ const o = value;
812
+ if (typeof o.key !== "string" || !o.key || typeof o.label !== "string") return [];
813
+ return [{ key: o.key, label: o.label }];
814
+ }
815
+ function parseIntegrations(value) {
816
+ if (!value || typeof value !== "object") return {};
817
+ const autoleap = value.autoleap;
818
+ if (!autoleap || typeof autoleap !== "object") return {};
819
+ const booking = autoleap.booking;
820
+ if (!booking || typeof booking !== "object") return {};
821
+ const b = booking;
822
+ if (typeof b.fallback_url !== "string" || !b.fallback_url) return {};
823
+ return {
824
+ autoleap: {
825
+ booking: {
826
+ enabled: b.enabled === true,
827
+ fallback_url: b.fallback_url,
828
+ timezone: typeof b.timezone === "string" ? b.timezone : "UTC",
829
+ config_version: typeof b.config_version === "number" ? b.config_version : null,
830
+ accepts_bookings: b.accepts_bookings === true,
831
+ rules: parseBookingRules(b.rules),
832
+ options: Array.isArray(b.options) ? b.options.flatMap(parseBookingOption) : [],
833
+ ...isBookingMode(b.mode) ? { mode: b.mode } : {}
834
+ }
835
+ }
836
+ };
837
+ }
721
838
  function parseTrigger(value) {
722
839
  if (!value || typeof value !== "object") return null;
723
840
  const t = value;
@@ -777,7 +894,8 @@ function parseConversionConfig(raw) {
777
894
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
778
895
  services,
779
896
  goals,
780
- linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : []
897
+ linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : [],
898
+ integrations: parseIntegrations(obj.integrations)
781
899
  };
782
900
  }
783
901
  var CACHE_PREFIX = "_aranova_cfg_";
@@ -1224,6 +1342,13 @@ var TrackingConfigRuntime = class {
1224
1342
  linkedHostnames() {
1225
1343
  return this.current?.linked_hostnames ?? [];
1226
1344
  }
1345
+ /**
1346
+ * Integration blocks, from the cached object even while unconfirmed: the booking fallback
1347
+ * link is safe to show stale. Same reference until the next adopt, for `useSyncExternalStore`.
1348
+ */
1349
+ integrations() {
1350
+ return this.current?.integrations ?? null;
1351
+ }
1227
1352
  async revalidateNow() {
1228
1353
  try {
1229
1354
  const headers = {};
@@ -1393,6 +1518,106 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1393
1518
  return runtime;
1394
1519
  }
1395
1520
 
1521
+ // ../../contracts/autoleap-booking-links.json
1522
+ var autoleap_booking_links_default = {
1523
+ hosts: ["myalp.io"]
1524
+ };
1525
+
1526
+ // ../tracking-core/src/integrations-public.ts
1527
+ var HANDOFF_HOSTS = new Set(autoleap_booking_links_default.hosts);
1528
+ function getBookingIntegration(integrations) {
1529
+ const booking = integrations?.autoleap?.booking;
1530
+ if (!booking) return null;
1531
+ const rules = booking.rules ? camelRules(booking.rules) : null;
1532
+ return {
1533
+ provider: "autoleap",
1534
+ enabled: booking.enabled,
1535
+ fallbackUrl: booking.fallback_url,
1536
+ acceptsBookings: booking.accepts_bookings && rules !== null && booking.config_version !== null,
1537
+ timezone: booking.timezone,
1538
+ configVersion: booking.config_version,
1539
+ rules,
1540
+ options: booking.options.map((option) => ({ key: option.key, label: option.label })),
1541
+ mode: booking.mode ?? "live"
1542
+ };
1543
+ }
1544
+ function camelRules(rules) {
1545
+ return {
1546
+ minDaysAhead: rules.min_days_ahead,
1547
+ maxDaysAhead: rules.max_days_ahead,
1548
+ hours: rules.hours,
1549
+ closedDates: rules.closed_dates,
1550
+ specialHours: rules.special_hours,
1551
+ vehicleRequired: rules.vehicle_required,
1552
+ dropOffOptions: rules.drop_off_options
1553
+ };
1554
+ }
1555
+ var WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
1556
+ function shopToday(timeZone, now = /* @__PURE__ */ new Date()) {
1557
+ const parts = new Intl.DateTimeFormat("en-CA", {
1558
+ timeZone,
1559
+ year: "numeric",
1560
+ month: "2-digit",
1561
+ day: "2-digit"
1562
+ }).formatToParts(now);
1563
+ const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
1564
+ return `${part("year")}-${part("month")}-${part("day")}`;
1565
+ }
1566
+ function addDays(isoDate, days) {
1567
+ const [year, month, day] = isoDate.split("-").map(Number);
1568
+ const date = new Date(Date.UTC(year, month - 1, day + days));
1569
+ return date.toISOString().slice(0, 10);
1570
+ }
1571
+ function bookableSlots(rules, { timeZone, now = /* @__PURE__ */ new Date() }) {
1572
+ const today = shopToday(timeZone, now);
1573
+ const days = [];
1574
+ const closed = new Set(rules.closedDates);
1575
+ for (let offset = rules.minDaysAhead; offset <= rules.maxDaysAhead; offset += 1) {
1576
+ const date = addDays(today, offset);
1577
+ if (closed.has(date)) continue;
1578
+ const weekday = WEEKDAYS[(/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay()];
1579
+ const windows = rules.specialHours[date] ?? rules.hours[weekday];
1580
+ if (!windows || windows.morning.length === 0 && windows.afternoon.length === 0) continue;
1581
+ days.push({
1582
+ date,
1583
+ weekday,
1584
+ morning: [...windows.morning],
1585
+ afternoon: [...windows.afternoon]
1586
+ });
1587
+ }
1588
+ return days;
1589
+ }
1590
+ function buildBookingHandoffUrl(fallbackUrl, prefill = {}) {
1591
+ let url;
1592
+ try {
1593
+ url = new URL(fallbackUrl);
1594
+ } catch {
1595
+ return null;
1596
+ }
1597
+ if (url.protocol !== "https:" || !HANDOFF_HOSTS.has(url.hostname) || url.username || url.password || url.port) {
1598
+ return null;
1599
+ }
1600
+ const [firstName, lastName] = prefill.firstName?.trim() ? [prefill.firstName, prefill.lastName] : splitFullName(prefill.name);
1601
+ const values = [
1602
+ ["firstName", firstName?.trim()],
1603
+ ["lastName", lastName?.trim()],
1604
+ ["phone", prefill.phone ? nationalDigits(prefill.phone) : ""],
1605
+ ["email", prefill.email?.trim()]
1606
+ ];
1607
+ for (const [key, value] of values) {
1608
+ if (value) url.searchParams.set(key, value);
1609
+ }
1610
+ return url.toString();
1611
+ }
1612
+ function splitFullName(name) {
1613
+ const [first = "", ...rest] = (name ?? "").trim().split(/\s+/);
1614
+ return [first, rest.join(" ")];
1615
+ }
1616
+ function nationalDigits(phone) {
1617
+ const parsed = parsePhone(phone);
1618
+ return (parsed.isValid ? parsed.national : phone).replace(/\D/g, "");
1619
+ }
1620
+
1396
1621
  // ../tracking-core/src/events/semantics.ts
1397
1622
  var EVENT_SEMANTICS = {
1398
1623
  page_view: {
@@ -1499,6 +1724,7 @@ var AranovaApiError = class extends Error {
1499
1724
  this.status = options.status;
1500
1725
  this.code = options.code;
1501
1726
  this.requestId = options.requestId;
1727
+ this.context = options.context;
1502
1728
  }
1503
1729
  };
1504
1730
 
@@ -1525,19 +1751,24 @@ async function apiRequest(config, method, path, body, extraHeaders) {
1525
1751
  if (!response.ok) {
1526
1752
  let detail;
1527
1753
  let code;
1754
+ let context;
1528
1755
  try {
1529
1756
  const parsed = await response.json();
1530
1757
  if (parsed && typeof parsed === "object") {
1531
1758
  const record3 = parsed;
1532
1759
  if (typeof record3.detail === "string") detail = record3.detail;
1533
1760
  if (typeof record3.code === "string") code = record3.code;
1761
+ if (record3.context && typeof record3.context === "object") {
1762
+ context = record3.context;
1763
+ }
1534
1764
  }
1535
1765
  } catch {
1536
1766
  }
1537
1767
  throw new AranovaApiError(detail ?? response.statusText ?? "Request failed", {
1538
1768
  status: response.status,
1539
1769
  code,
1540
- requestId: response.headers.get("x-request-id") ?? void 0
1770
+ requestId: response.headers.get("x-request-id") ?? void 0,
1771
+ context
1541
1772
  });
1542
1773
  }
1543
1774
  if (response.status === 204) return void 0;
@@ -1547,6 +1778,214 @@ async function apiRequest(config, method, path, body, extraHeaders) {
1547
1778
  // ../tracking-core/src/resources/sales/transport.ts
1548
1779
  var salesRequest = apiRequest;
1549
1780
 
1781
+ // ../tracking-core/src/resources/sales/bookings.ts
1782
+ var BOOKING_SIMULATE_HEADER = "X-Aranova-Simulate";
1783
+ var BOOKING_ERROR_CODES = {
1784
+ unavailable: "booking_unavailable",
1785
+ stale: "booking_config_stale",
1786
+ rejected: "booking_rejected"
1787
+ };
1788
+ var POLL_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
1789
+ var INVALID_DETAILS = "Please check your details and try again.";
1790
+ var newBookingAttemptKey = uuidV4;
1791
+ function sleep(ms) {
1792
+ return new Promise((resolve) => {
1793
+ setTimeout(resolve, ms);
1794
+ });
1795
+ }
1796
+ function outcomeOf(body) {
1797
+ const mode = body.mode ?? "live";
1798
+ if (body.status === "created")
1799
+ return { status: "created", id: body.id, requestCode: body.request_code, mode };
1800
+ if (body.status === "pending") return { status: "pending", id: body.id, mode };
1801
+ return { status: "failed", id: body.id, fallbackUrl: body.fallback_url, mode };
1802
+ }
1803
+ function formSubmitPayload(input, formId, labels) {
1804
+ const vehicle = input.vehicle ? `${input.vehicle.year} ${input.vehicle.make} ${input.vehicle.model}` : "";
1805
+ const field = (name, type, label, value) => ({
1806
+ name,
1807
+ type,
1808
+ label,
1809
+ value
1810
+ });
1811
+ return {
1812
+ form: {
1813
+ id: formId,
1814
+ action: "/tracking/bookings",
1815
+ fields: [
1816
+ field("first_name", "text", "First name", input.firstName),
1817
+ field("last_name", "text", "Last name", input.lastName),
1818
+ // E.164 on the wire: it keys the customer and enhanced-conversion matching.
1819
+ field("phone", "tel", "Phone", toE164(input.phone) ?? input.phone),
1820
+ field("email", "email", "Email", input.email ?? ""),
1821
+ field(
1822
+ "service",
1823
+ "select",
1824
+ "Service",
1825
+ input.services.map((key) => labels.get(key) ?? key).join(", ")
1826
+ ),
1827
+ field("vehicle", "text", "Vehicle", vehicle),
1828
+ field(
1829
+ "start",
1830
+ "text",
1831
+ "Requested time",
1832
+ `${input.startDate} ${String(input.startHour).padStart(2, "0")}:00`
1833
+ ),
1834
+ field("note", "textarea", "Notes", input.notes ?? "")
1835
+ ]
1836
+ },
1837
+ page: { path: typeof window === "undefined" ? "/" : window.location.pathname }
1838
+ };
1839
+ }
1840
+ function runtimeOf(firing) {
1841
+ return firing && "integrations" in firing ? firing : null;
1842
+ }
1843
+ function isLocalUrl(url) {
1844
+ try {
1845
+ return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname);
1846
+ } catch {
1847
+ return false;
1848
+ }
1849
+ }
1850
+ function bookingSetupMismatch(endpoint, runtime) {
1851
+ if (!runtime || !isLocalUrl(endpoint)) return null;
1852
+ const configUrl = resolveTrackingConfigUrl(runtime.ref);
1853
+ if (isLocalUrl(configUrl)) return null;
1854
+ return `[aranova] Bookings go to the local API (${endpoint}) but the runtime config comes from ${configUrl}. Set NEXT_PUBLIC_ARANOVA_CDN_BASE_URL to the local bucket (http://localhost:9100/aranova-demos) so both come from one backend.`;
1855
+ }
1856
+ function createSalesBookingsClient(config, firing) {
1857
+ const submittedForms = /* @__PURE__ */ new Set();
1858
+ const staleAttempts = /* @__PURE__ */ new Set();
1859
+ let isSetupChecked = false;
1860
+ function warnOnSetupMismatch(runtime) {
1861
+ if (isSetupChecked) return;
1862
+ isSetupChecked = true;
1863
+ const warning = bookingSetupMismatch(config.endpoint, runtime);
1864
+ if (warning) console.warn(warning);
1865
+ }
1866
+ function handoff(input) {
1867
+ const booking = getBookingIntegration(runtimeOf(firing)?.integrations());
1868
+ if (!booking) return null;
1869
+ return buildBookingHandoffUrl(booking.fallbackUrl, {
1870
+ firstName: input.firstName,
1871
+ lastName: input.lastName,
1872
+ phone: input.phone,
1873
+ email: input.email
1874
+ });
1875
+ }
1876
+ function failedBeforeSend(input) {
1877
+ const mode = getBookingIntegration(runtimeOf(firing)?.integrations())?.mode ?? "live";
1878
+ return { status: "failed", id: null, fallbackUrl: handoff(input), mode };
1879
+ }
1880
+ function withHandoff(outcome, input) {
1881
+ if (outcome.status === "failed" && !outcome.fallbackUrl) {
1882
+ return { ...outcome, fallbackUrl: handoff(input) };
1883
+ }
1884
+ return outcome;
1885
+ }
1886
+ async function poll(id, mode) {
1887
+ let outcome = { status: "pending", id, mode };
1888
+ for (const delay of POLL_DELAYS_MS) {
1889
+ await sleep(delay);
1890
+ try {
1891
+ const path = `/bookings/${encodeURIComponent(id)}`;
1892
+ outcome = outcomeOf(await salesRequest(config, "GET", path));
1893
+ } catch {
1894
+ continue;
1895
+ }
1896
+ if (outcome.status !== "pending") return outcome;
1897
+ }
1898
+ return outcome;
1899
+ }
1900
+ async function rejected(error, input, attemptKey, runtime) {
1901
+ if (!(error instanceof AranovaApiError)) {
1902
+ return failedBeforeSend(input);
1903
+ }
1904
+ if (error.code === BOOKING_ERROR_CODES.stale) {
1905
+ if (staleAttempts.has(attemptKey)) return failedBeforeSend(input);
1906
+ staleAttempts.add(attemptKey);
1907
+ await runtime?.revalidate();
1908
+ return { status: "stale", message: error.message };
1909
+ }
1910
+ if (error.code === BOOKING_ERROR_CODES.rejected) {
1911
+ const reasons = error.context?.reasons;
1912
+ return {
1913
+ status: "invalid",
1914
+ message: error.message,
1915
+ reasons: Array.isArray(reasons) ? reasons.map(String) : []
1916
+ };
1917
+ }
1918
+ if (error.code === "validation_error") {
1919
+ return { status: "invalid", message: INVALID_DETAILS, reasons: [] };
1920
+ }
1921
+ return failedBeforeSend(input);
1922
+ }
1923
+ return {
1924
+ async submit(input, { attemptKey, formSubmit, simulate }) {
1925
+ registerCapability("booking_integration");
1926
+ const runtime = runtimeOf(firing);
1927
+ warnOnSetupMismatch(runtime);
1928
+ const booking = getBookingIntegration(runtime?.integrations());
1929
+ if (!booking?.acceptsBookings || booking.configVersion === null) {
1930
+ return failedBeforeSend(input);
1931
+ }
1932
+ if (formSubmit && !submittedForms.has(attemptKey)) {
1933
+ submittedForms.add(attemptKey);
1934
+ const labels = new Map(booking.options.map((option) => [option.key, option.label]));
1935
+ formSubmit.tracking.trackEvent(
1936
+ "form_submit",
1937
+ formSubmitPayload(input, formSubmit.formId, labels)
1938
+ );
1939
+ }
1940
+ const body = {
1941
+ idempotency_key: attemptKey,
1942
+ // The tracking client's own visit, so the booking and its form_submit share one session.
1943
+ session_id: formSubmit?.tracking.getSessionId() ?? peekSessionId() ?? `no-session-${attemptKey}`,
1944
+ config_version: booking.configVersion,
1945
+ start_date: input.startDate,
1946
+ start_hour: input.startHour,
1947
+ first_name: input.firstName.trim(),
1948
+ last_name: input.lastName.trim(),
1949
+ phone: input.phone,
1950
+ email: input.email?.trim() || null,
1951
+ vehicle: input.vehicle ?? null,
1952
+ drop_off: input.dropOff ?? null,
1953
+ services: input.services,
1954
+ notes: input.notes?.trim() || null,
1955
+ marketing_sms_consent: input.marketingSmsConsent === true,
1956
+ page_path: typeof window === "undefined" ? null : window.location.pathname
1957
+ };
1958
+ let response;
1959
+ try {
1960
+ const headers = simulate ? { [BOOKING_SIMULATE_HEADER]: simulate } : void 0;
1961
+ response = await salesRequest(
1962
+ config,
1963
+ "POST",
1964
+ "/bookings",
1965
+ body,
1966
+ headers
1967
+ );
1968
+ } catch (error) {
1969
+ return rejected(error, input, attemptKey, runtime);
1970
+ }
1971
+ const outcome = outcomeOf(response);
1972
+ return withHandoff(
1973
+ outcome.status === "pending" ? await poll(outcome.id, outcome.mode) : outcome,
1974
+ input
1975
+ );
1976
+ },
1977
+ async status(id) {
1978
+ return outcomeOf(
1979
+ await salesRequest(
1980
+ config,
1981
+ "GET",
1982
+ `/bookings/${encodeURIComponent(id)}`
1983
+ )
1984
+ );
1985
+ }
1986
+ };
1987
+ }
1988
+
1550
1989
  // ../tracking-core/src/resources/sales/client.ts
1551
1990
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
1552
1991
  if (!firing) return;
@@ -1721,7 +2160,8 @@ function createSalesClient(config) {
1721
2160
  async config() {
1722
2161
  return salesRequest(config, "GET", "/business/config");
1723
2162
  }
1724
- }
2163
+ },
2164
+ bookings: createSalesBookingsClient(config, config.firing)
1725
2165
  };
1726
2166
  }
1727
2167
 
@@ -1730,6 +2170,12 @@ var z3 = __toESM(require("zod/mini"));
1730
2170
 
1731
2171
  // ../tracking-core/src/resources/sales/generated/zod.gen.ts
1732
2172
  var z2 = __toESM(require("zod/mini"));
2173
+ var zAutoLeapSaleFacts = z2.object({
2174
+ first_order: z2.boolean(),
2175
+ fleet_customer: z2.boolean(),
2176
+ invoice_code: z2.string(),
2177
+ invoice_id: z2.string()
2178
+ });
1733
2179
  var zErrorCode = z2.enum([
1734
2180
  "missing_credentials",
1735
2181
  "invalid_api_key",
@@ -1757,6 +2203,9 @@ var zErrorCode = z2.enum([
1757
2203
  "booking_not_reschedulable",
1758
2204
  "manage_token_invalid",
1759
2205
  "google_error",
2206
+ "booking_unavailable",
2207
+ "booking_config_stale",
2208
+ "booking_rejected",
1760
2209
  "calendar_not_owned",
1761
2210
  "calendar_is_sandbox",
1762
2211
  "sandbox_exists",
@@ -1798,6 +2247,7 @@ var zSaleServiceInputSchema = z2.strictObject({
1798
2247
  amount_cents: z2.int().check(z2.gte(0)),
1799
2248
  service: z2.string()
1800
2249
  });
2250
+ var zSaleSource = z2.enum(["api", "admin", "autoleap"]);
1801
2251
  var zSmsAutomationState = z2.enum([
1802
2252
  "needs_check",
1803
2253
  "failed",
@@ -1829,7 +2279,8 @@ var zSmsWithheldReason = z2.enum([
1829
2279
  "number_not_active",
1830
2280
  "a2p_registration_required",
1831
2281
  "alert_recipient_cannot_receive_sms",
1832
- "line_type_unresolved"
2282
+ "line_type_unresolved",
2283
+ "sale_source_sends_own_messages"
1833
2284
  ]);
1834
2285
  var zSaleSmsStatusSchema = z2.object({
1835
2286
  can_send_now: z2._default(z2.boolean(), false),
@@ -1938,6 +2389,8 @@ var zSaleEntitySchema = z2.object({
1938
2389
  sms_automation: z2.nullable(zSaleSmsStatusSchema),
1939
2390
  sms_consent: z2._default(z2.boolean(), false),
1940
2391
  sms_consent_granted_at: z2.nullable(z2.iso.datetime({ offset: true })),
2392
+ source: zSaleSource,
2393
+ source_facts: z2.nullable(zAutoLeapSaleFacts),
1941
2394
  updated_at: z2.iso.datetime({ offset: true }),
1942
2395
  writeback: z2.nullable(zSaleWritebackStatusSchema)
1943
2396
  });
@@ -1947,6 +2400,7 @@ var saleItemSchema = zSaleItemInputSchema;
1947
2400
  var saleServiceSchema = zSaleServiceInputSchema;
1948
2401
  var currencySchema = zSupportedCurrency;
1949
2402
  var SUPPORTED_CURRENCIES = currencySchema.options;
2403
+ var SALE_SOURCES = zSaleSource.options;
1950
2404
  var centsSchema = z3.int().check(z3.gte(0));
1951
2405
  var metadataSchema = z3.record(z3.string(), z3.unknown());
1952
2406
  var customerNameSchema = z3.string().check(z3.maxLength(200));
@@ -2031,10 +2485,12 @@ function phoneField(name, raw, country) {
2031
2485
 
2032
2486
  // src/index.ts
2033
2487
  var import_client2 = require("@aranova/tracking-react/client");
2488
+ var import_client3 = require("@aranova/tracking-react/client");
2034
2489
  // Annotate the CommonJS export names for ESM import in node:
2035
2490
  0 && (module.exports = {
2036
2491
  AdPlatformTracking,
2037
2492
  AranovaApiError,
2493
+ BookingHandoffLink,
2038
2494
  ConsentBanner,
2039
2495
  DEFAULT_DECLINE_TTL_DAYS,
2040
2496
  DEFAULT_PHONE_COUNTRY,
@@ -2042,9 +2498,12 @@ var import_client2 = require("@aranova/tracking-react/client");
2042
2498
  GoogleAdsTracking,
2043
2499
  NAMED_RANGES,
2044
2500
  PhoneField,
2501
+ SALE_SOURCES,
2045
2502
  SUPPORTED_CURRENCIES,
2046
2503
  TRACKING_PARAM_KEYS,
2047
2504
  TRACKING_RANGES,
2505
+ bookableSlots,
2506
+ buildBookingHandoffUrl,
2048
2507
  captureTrackingParamsFromLocation,
2049
2508
  clearStashedUserData,
2050
2509
  createSalesClient,
@@ -2059,10 +2518,12 @@ var import_client2 = require("@aranova/tracking-react/client");
2059
2518
  formatPhone,
2060
2519
  formatPhoneAsTyped,
2061
2520
  fromMinor,
2521
+ getBookingIntegration,
2062
2522
  getConsentChoice,
2063
2523
  getConsentState,
2064
2524
  getEventSemantics,
2065
2525
  getTrackingConfigRuntime,
2526
+ newBookingAttemptKey,
2066
2527
  onConsentChange,
2067
2528
  optIn,
2068
2529
  optOut,
@@ -2077,9 +2538,14 @@ var import_client2 = require("@aranova/tracking-react/client");
2077
2538
  saleUpdateSchema,
2078
2539
  salesRequest,
2079
2540
  setConsentState,
2541
+ shopToday,
2080
2542
  stashUserData,
2081
2543
  toE164,
2082
2544
  toMinor,
2545
+ useBookingHandoffUrl,
2546
+ useBookingIntegration,
2547
+ useBookingSlots,
2548
+ useBookingSubmit,
2083
2549
  useConsent,
2084
2550
  useConsentState,
2085
2551
  useCookiePreferences,