@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/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;
@@ -718,6 +767,69 @@ function parseFiring(value) {
718
767
  currency: typeof f.currency === "string" ? f.currency : null
719
768
  };
720
769
  }
770
+ function isNumberList(value) {
771
+ return Array.isArray(value) && value.every((v) => typeof v === "number");
772
+ }
773
+ function isStringList(value) {
774
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
775
+ }
776
+ function parseWindowsMap(value) {
777
+ const windowsByKey = {};
778
+ if (!value || typeof value !== "object") return windowsByKey;
779
+ for (const [key, windows] of Object.entries(value)) {
780
+ if (!windows || typeof windows !== "object") continue;
781
+ const w = windows;
782
+ windowsByKey[key] = {
783
+ morning: isNumberList(w.morning) ? w.morning : [],
784
+ afternoon: isNumberList(w.afternoon) ? w.afternoon : []
785
+ };
786
+ }
787
+ return windowsByKey;
788
+ }
789
+ function parseBookingRules(value) {
790
+ if (!value || typeof value !== "object") return null;
791
+ const r = value;
792
+ if (typeof r.min_days_ahead !== "number" || typeof r.max_days_ahead !== "number") return null;
793
+ if (!r.hours || typeof r.hours !== "object") return null;
794
+ const hours = parseWindowsMap(r.hours);
795
+ return {
796
+ min_days_ahead: r.min_days_ahead,
797
+ max_days_ahead: r.max_days_ahead,
798
+ hours,
799
+ closed_dates: isStringList(r.closed_dates) ? r.closed_dates : [],
800
+ special_hours: parseWindowsMap(r.special_hours),
801
+ vehicle_required: r.vehicle_required === true,
802
+ drop_off_options: isStringList(r.drop_off_options) ? r.drop_off_options : []
803
+ };
804
+ }
805
+ function parseBookingOption(value) {
806
+ if (!value || typeof value !== "object") return [];
807
+ const o = value;
808
+ if (typeof o.key !== "string" || !o.key || typeof o.label !== "string") return [];
809
+ return [{ key: o.key, label: o.label }];
810
+ }
811
+ function parseIntegrations(value) {
812
+ if (!value || typeof value !== "object") return {};
813
+ const autoleap = value.autoleap;
814
+ if (!autoleap || typeof autoleap !== "object") return {};
815
+ const booking = autoleap.booking;
816
+ if (!booking || typeof booking !== "object") return {};
817
+ const b = booking;
818
+ if (typeof b.fallback_url !== "string" || !b.fallback_url) return {};
819
+ return {
820
+ autoleap: {
821
+ booking: {
822
+ enabled: b.enabled === true,
823
+ fallback_url: b.fallback_url,
824
+ timezone: typeof b.timezone === "string" ? b.timezone : "UTC",
825
+ config_version: typeof b.config_version === "number" ? b.config_version : null,
826
+ accepts_bookings: b.accepts_bookings === true,
827
+ rules: parseBookingRules(b.rules),
828
+ options: Array.isArray(b.options) ? b.options.flatMap(parseBookingOption) : []
829
+ }
830
+ }
831
+ };
832
+ }
721
833
  function parseTrigger(value) {
722
834
  if (!value || typeof value !== "object") return null;
723
835
  const t = value;
@@ -777,7 +889,8 @@ function parseConversionConfig(raw) {
777
889
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
778
890
  services,
779
891
  goals,
780
- linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : []
892
+ linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : [],
893
+ integrations: parseIntegrations(obj.integrations)
781
894
  };
782
895
  }
783
896
  var CACHE_PREFIX = "_aranova_cfg_";
@@ -1224,6 +1337,13 @@ var TrackingConfigRuntime = class {
1224
1337
  linkedHostnames() {
1225
1338
  return this.current?.linked_hostnames ?? [];
1226
1339
  }
1340
+ /**
1341
+ * Integration blocks, from the cached object even while unconfirmed: the booking fallback
1342
+ * link is safe to show stale. Same reference until the next adopt, for `useSyncExternalStore`.
1343
+ */
1344
+ integrations() {
1345
+ return this.current?.integrations ?? null;
1346
+ }
1227
1347
  async revalidateNow() {
1228
1348
  try {
1229
1349
  const headers = {};
@@ -1393,6 +1513,105 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1393
1513
  return runtime;
1394
1514
  }
1395
1515
 
1516
+ // ../../contracts/autoleap-booking-links.json
1517
+ var autoleap_booking_links_default = {
1518
+ hosts: ["myalp.io"]
1519
+ };
1520
+
1521
+ // ../tracking-core/src/integrations-public.ts
1522
+ var HANDOFF_HOSTS = new Set(autoleap_booking_links_default.hosts);
1523
+ function getBookingIntegration(integrations) {
1524
+ const booking = integrations?.autoleap?.booking;
1525
+ if (!booking) return null;
1526
+ const rules = booking.rules ? camelRules(booking.rules) : null;
1527
+ return {
1528
+ provider: "autoleap",
1529
+ enabled: booking.enabled,
1530
+ fallbackUrl: booking.fallback_url,
1531
+ acceptsBookings: booking.accepts_bookings && rules !== null && booking.config_version !== null,
1532
+ timezone: booking.timezone,
1533
+ configVersion: booking.config_version,
1534
+ rules,
1535
+ options: booking.options.map((option) => ({ key: option.key, label: option.label }))
1536
+ };
1537
+ }
1538
+ function camelRules(rules) {
1539
+ return {
1540
+ minDaysAhead: rules.min_days_ahead,
1541
+ maxDaysAhead: rules.max_days_ahead,
1542
+ hours: rules.hours,
1543
+ closedDates: rules.closed_dates,
1544
+ specialHours: rules.special_hours,
1545
+ vehicleRequired: rules.vehicle_required,
1546
+ dropOffOptions: rules.drop_off_options
1547
+ };
1548
+ }
1549
+ var WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
1550
+ function shopToday(timeZone, now = /* @__PURE__ */ new Date()) {
1551
+ const parts = new Intl.DateTimeFormat("en-CA", {
1552
+ timeZone,
1553
+ year: "numeric",
1554
+ month: "2-digit",
1555
+ day: "2-digit"
1556
+ }).formatToParts(now);
1557
+ const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
1558
+ return `${part("year")}-${part("month")}-${part("day")}`;
1559
+ }
1560
+ function addDays(isoDate, days) {
1561
+ const [year, month, day] = isoDate.split("-").map(Number);
1562
+ const date = new Date(Date.UTC(year, month - 1, day + days));
1563
+ return date.toISOString().slice(0, 10);
1564
+ }
1565
+ function bookableSlots(rules, { timeZone, now = /* @__PURE__ */ new Date() }) {
1566
+ const today = shopToday(timeZone, now);
1567
+ const days = [];
1568
+ const closed = new Set(rules.closedDates);
1569
+ for (let offset = rules.minDaysAhead; offset <= rules.maxDaysAhead; offset += 1) {
1570
+ const date = addDays(today, offset);
1571
+ if (closed.has(date)) continue;
1572
+ const weekday = WEEKDAYS[(/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay()];
1573
+ const windows = rules.specialHours[date] ?? rules.hours[weekday];
1574
+ if (!windows || windows.morning.length === 0 && windows.afternoon.length === 0) continue;
1575
+ days.push({
1576
+ date,
1577
+ weekday,
1578
+ morning: [...windows.morning],
1579
+ afternoon: [...windows.afternoon]
1580
+ });
1581
+ }
1582
+ return days;
1583
+ }
1584
+ function buildBookingHandoffUrl(fallbackUrl, prefill = {}) {
1585
+ let url;
1586
+ try {
1587
+ url = new URL(fallbackUrl);
1588
+ } catch {
1589
+ return null;
1590
+ }
1591
+ if (url.protocol !== "https:" || !HANDOFF_HOSTS.has(url.hostname) || url.username || url.password || url.port) {
1592
+ return null;
1593
+ }
1594
+ const [firstName, lastName] = prefill.firstName?.trim() ? [prefill.firstName, prefill.lastName] : splitFullName(prefill.name);
1595
+ const values = [
1596
+ ["firstName", firstName?.trim()],
1597
+ ["lastName", lastName?.trim()],
1598
+ ["phone", prefill.phone ? nationalDigits(prefill.phone) : ""],
1599
+ ["email", prefill.email?.trim()]
1600
+ ];
1601
+ for (const [key, value] of values) {
1602
+ if (value) url.searchParams.set(key, value);
1603
+ }
1604
+ return url.toString();
1605
+ }
1606
+ function splitFullName(name) {
1607
+ const [first = "", ...rest] = (name ?? "").trim().split(/\s+/);
1608
+ return [first, rest.join(" ")];
1609
+ }
1610
+ function nationalDigits(phone) {
1611
+ const parsed = parsePhone(phone);
1612
+ return (parsed.isValid ? parsed.national : phone).replace(/\D/g, "");
1613
+ }
1614
+
1396
1615
  // ../tracking-core/src/events/semantics.ts
1397
1616
  var EVENT_SEMANTICS = {
1398
1617
  page_view: {
@@ -1499,6 +1718,7 @@ var AranovaApiError = class extends Error {
1499
1718
  this.status = options.status;
1500
1719
  this.code = options.code;
1501
1720
  this.requestId = options.requestId;
1721
+ this.context = options.context;
1502
1722
  }
1503
1723
  };
1504
1724
 
@@ -1525,19 +1745,24 @@ async function apiRequest(config, method, path, body, extraHeaders) {
1525
1745
  if (!response.ok) {
1526
1746
  let detail;
1527
1747
  let code;
1748
+ let context;
1528
1749
  try {
1529
1750
  const parsed = await response.json();
1530
1751
  if (parsed && typeof parsed === "object") {
1531
1752
  const record3 = parsed;
1532
1753
  if (typeof record3.detail === "string") detail = record3.detail;
1533
1754
  if (typeof record3.code === "string") code = record3.code;
1755
+ if (record3.context && typeof record3.context === "object") {
1756
+ context = record3.context;
1757
+ }
1534
1758
  }
1535
1759
  } catch {
1536
1760
  }
1537
1761
  throw new AranovaApiError(detail ?? response.statusText ?? "Request failed", {
1538
1762
  status: response.status,
1539
1763
  code,
1540
- requestId: response.headers.get("x-request-id") ?? void 0
1764
+ requestId: response.headers.get("x-request-id") ?? void 0,
1765
+ context
1541
1766
  });
1542
1767
  }
1543
1768
  if (response.status === 204) return void 0;
@@ -1547,6 +1772,179 @@ async function apiRequest(config, method, path, body, extraHeaders) {
1547
1772
  // ../tracking-core/src/resources/sales/transport.ts
1548
1773
  var salesRequest = apiRequest;
1549
1774
 
1775
+ // ../tracking-core/src/resources/sales/bookings.ts
1776
+ var BOOKING_ERROR_CODES = {
1777
+ unavailable: "booking_unavailable",
1778
+ stale: "booking_config_stale",
1779
+ rejected: "booking_rejected"
1780
+ };
1781
+ var POLL_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
1782
+ var INVALID_DETAILS = "Please check your details and try again.";
1783
+ var newBookingAttemptKey = uuidV4;
1784
+ function sleep(ms) {
1785
+ return new Promise((resolve) => {
1786
+ setTimeout(resolve, ms);
1787
+ });
1788
+ }
1789
+ function outcomeOf(body) {
1790
+ if (body.status === "created")
1791
+ return { status: "created", id: body.id, requestCode: body.request_code };
1792
+ if (body.status === "pending") return { status: "pending", id: body.id };
1793
+ return { status: "failed", id: body.id, fallbackUrl: body.fallback_url };
1794
+ }
1795
+ function formSubmitPayload(input, formId, labels) {
1796
+ const vehicle = input.vehicle ? `${input.vehicle.year} ${input.vehicle.make} ${input.vehicle.model}` : "";
1797
+ const field = (name, type, label, value) => ({
1798
+ name,
1799
+ type,
1800
+ label,
1801
+ value
1802
+ });
1803
+ return {
1804
+ form: {
1805
+ id: formId,
1806
+ action: "/tracking/bookings",
1807
+ fields: [
1808
+ field("first_name", "text", "First name", input.firstName),
1809
+ field("last_name", "text", "Last name", input.lastName),
1810
+ // E.164 on the wire: it keys the customer and enhanced-conversion matching.
1811
+ field("phone", "tel", "Phone", toE164(input.phone) ?? input.phone),
1812
+ field("email", "email", "Email", input.email ?? ""),
1813
+ field(
1814
+ "service",
1815
+ "select",
1816
+ "Service",
1817
+ input.services.map((key) => labels.get(key) ?? key).join(", ")
1818
+ ),
1819
+ field("vehicle", "text", "Vehicle", vehicle),
1820
+ field(
1821
+ "start",
1822
+ "text",
1823
+ "Requested time",
1824
+ `${input.startDate} ${String(input.startHour).padStart(2, "0")}:00`
1825
+ ),
1826
+ field("note", "textarea", "Notes", input.notes ?? "")
1827
+ ]
1828
+ },
1829
+ page: { path: typeof window === "undefined" ? "/" : window.location.pathname }
1830
+ };
1831
+ }
1832
+ function runtimeOf(firing) {
1833
+ return firing && "integrations" in firing ? firing : null;
1834
+ }
1835
+ function createSalesBookingsClient(config, firing) {
1836
+ const submittedForms = /* @__PURE__ */ new Set();
1837
+ const staleAttempts = /* @__PURE__ */ new Set();
1838
+ function handoff(input) {
1839
+ const booking = getBookingIntegration(runtimeOf(firing)?.integrations());
1840
+ if (!booking) return null;
1841
+ return buildBookingHandoffUrl(booking.fallbackUrl, {
1842
+ firstName: input.firstName,
1843
+ lastName: input.lastName,
1844
+ phone: input.phone,
1845
+ email: input.email
1846
+ });
1847
+ }
1848
+ function withHandoff(outcome, input) {
1849
+ if (outcome.status === "failed" && !outcome.fallbackUrl) {
1850
+ return { ...outcome, fallbackUrl: handoff(input) };
1851
+ }
1852
+ return outcome;
1853
+ }
1854
+ async function poll(id) {
1855
+ let outcome = { status: "pending", id };
1856
+ for (const delay of POLL_DELAYS_MS) {
1857
+ await sleep(delay);
1858
+ try {
1859
+ const path = `/bookings/${encodeURIComponent(id)}`;
1860
+ outcome = outcomeOf(await salesRequest(config, "GET", path));
1861
+ } catch {
1862
+ continue;
1863
+ }
1864
+ if (outcome.status !== "pending") return outcome;
1865
+ }
1866
+ return outcome;
1867
+ }
1868
+ async function rejected(error, input, attemptKey, runtime) {
1869
+ if (!(error instanceof AranovaApiError)) {
1870
+ return { status: "failed", id: null, fallbackUrl: handoff(input) };
1871
+ }
1872
+ if (error.code === BOOKING_ERROR_CODES.stale) {
1873
+ if (staleAttempts.has(attemptKey)) {
1874
+ return { status: "failed", id: null, fallbackUrl: handoff(input) };
1875
+ }
1876
+ staleAttempts.add(attemptKey);
1877
+ await runtime?.revalidate();
1878
+ return { status: "stale", message: error.message };
1879
+ }
1880
+ if (error.code === BOOKING_ERROR_CODES.rejected) {
1881
+ const reasons = error.context?.reasons;
1882
+ return {
1883
+ status: "invalid",
1884
+ message: error.message,
1885
+ reasons: Array.isArray(reasons) ? reasons.map(String) : []
1886
+ };
1887
+ }
1888
+ if (error.code === "validation_error") {
1889
+ return { status: "invalid", message: INVALID_DETAILS, reasons: [] };
1890
+ }
1891
+ return { status: "failed", id: null, fallbackUrl: handoff(input) };
1892
+ }
1893
+ return {
1894
+ async submit(input, { attemptKey, formSubmit }) {
1895
+ registerCapability("booking_integration");
1896
+ const runtime = runtimeOf(firing);
1897
+ const booking = getBookingIntegration(runtime?.integrations());
1898
+ if (!booking?.acceptsBookings || booking.configVersion === null) {
1899
+ return { status: "failed", id: null, fallbackUrl: handoff(input) };
1900
+ }
1901
+ if (formSubmit && !submittedForms.has(attemptKey)) {
1902
+ submittedForms.add(attemptKey);
1903
+ const labels = new Map(booking.options.map((option) => [option.key, option.label]));
1904
+ formSubmit.tracking.trackEvent(
1905
+ "form_submit",
1906
+ formSubmitPayload(input, formSubmit.formId, labels)
1907
+ );
1908
+ }
1909
+ const body = {
1910
+ idempotency_key: attemptKey,
1911
+ // The tracking client's own visit, so the booking and its form_submit share one session.
1912
+ session_id: formSubmit?.tracking.getSessionId() ?? peekSessionId() ?? `no-session-${attemptKey}`,
1913
+ config_version: booking.configVersion,
1914
+ start_date: input.startDate,
1915
+ start_hour: input.startHour,
1916
+ first_name: input.firstName.trim(),
1917
+ last_name: input.lastName.trim(),
1918
+ phone: input.phone,
1919
+ email: input.email?.trim() || null,
1920
+ vehicle: input.vehicle ?? null,
1921
+ drop_off: input.dropOff ?? null,
1922
+ services: input.services,
1923
+ notes: input.notes?.trim() || null,
1924
+ marketing_sms_consent: input.marketingSmsConsent === true,
1925
+ page_path: typeof window === "undefined" ? null : window.location.pathname
1926
+ };
1927
+ let response;
1928
+ try {
1929
+ response = await salesRequest(config, "POST", "/bookings", body);
1930
+ } catch (error) {
1931
+ return rejected(error, input, attemptKey, runtime);
1932
+ }
1933
+ const outcome = outcomeOf(response);
1934
+ return withHandoff(outcome.status === "pending" ? await poll(outcome.id) : outcome, input);
1935
+ },
1936
+ async status(id) {
1937
+ return outcomeOf(
1938
+ await salesRequest(
1939
+ config,
1940
+ "GET",
1941
+ `/bookings/${encodeURIComponent(id)}`
1942
+ )
1943
+ );
1944
+ }
1945
+ };
1946
+ }
1947
+
1550
1948
  // ../tracking-core/src/resources/sales/client.ts
1551
1949
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
1552
1950
  if (!firing) return;
@@ -1721,7 +2119,8 @@ function createSalesClient(config) {
1721
2119
  async config() {
1722
2120
  return salesRequest(config, "GET", "/business/config");
1723
2121
  }
1724
- }
2122
+ },
2123
+ bookings: createSalesBookingsClient(config, config.firing)
1725
2124
  };
1726
2125
  }
1727
2126
 
@@ -1730,6 +2129,12 @@ var z3 = __toESM(require("zod/mini"));
1730
2129
 
1731
2130
  // ../tracking-core/src/resources/sales/generated/zod.gen.ts
1732
2131
  var z2 = __toESM(require("zod/mini"));
2132
+ var zAutoLeapSaleFacts = z2.object({
2133
+ first_order: z2.boolean(),
2134
+ fleet_customer: z2.boolean(),
2135
+ invoice_code: z2.string(),
2136
+ invoice_id: z2.string()
2137
+ });
1733
2138
  var zErrorCode = z2.enum([
1734
2139
  "missing_credentials",
1735
2140
  "invalid_api_key",
@@ -1757,6 +2162,9 @@ var zErrorCode = z2.enum([
1757
2162
  "booking_not_reschedulable",
1758
2163
  "manage_token_invalid",
1759
2164
  "google_error",
2165
+ "booking_unavailable",
2166
+ "booking_config_stale",
2167
+ "booking_rejected",
1760
2168
  "calendar_not_owned",
1761
2169
  "calendar_is_sandbox",
1762
2170
  "sandbox_exists",
@@ -1798,6 +2206,7 @@ var zSaleServiceInputSchema = z2.strictObject({
1798
2206
  amount_cents: z2.int().check(z2.gte(0)),
1799
2207
  service: z2.string()
1800
2208
  });
2209
+ var zSaleSource = z2.enum(["api", "admin", "autoleap"]);
1801
2210
  var zSmsAutomationState = z2.enum([
1802
2211
  "needs_check",
1803
2212
  "failed",
@@ -1829,7 +2238,8 @@ var zSmsWithheldReason = z2.enum([
1829
2238
  "number_not_active",
1830
2239
  "a2p_registration_required",
1831
2240
  "alert_recipient_cannot_receive_sms",
1832
- "line_type_unresolved"
2241
+ "line_type_unresolved",
2242
+ "sale_source_sends_own_messages"
1833
2243
  ]);
1834
2244
  var zSaleSmsStatusSchema = z2.object({
1835
2245
  can_send_now: z2._default(z2.boolean(), false),
@@ -1938,6 +2348,8 @@ var zSaleEntitySchema = z2.object({
1938
2348
  sms_automation: z2.nullable(zSaleSmsStatusSchema),
1939
2349
  sms_consent: z2._default(z2.boolean(), false),
1940
2350
  sms_consent_granted_at: z2.nullable(z2.iso.datetime({ offset: true })),
2351
+ source: zSaleSource,
2352
+ source_facts: z2.nullable(zAutoLeapSaleFacts),
1941
2353
  updated_at: z2.iso.datetime({ offset: true }),
1942
2354
  writeback: z2.nullable(zSaleWritebackStatusSchema)
1943
2355
  });
@@ -1947,6 +2359,7 @@ var saleItemSchema = zSaleItemInputSchema;
1947
2359
  var saleServiceSchema = zSaleServiceInputSchema;
1948
2360
  var currencySchema = zSupportedCurrency;
1949
2361
  var SUPPORTED_CURRENCIES = currencySchema.options;
2362
+ var SALE_SOURCES = zSaleSource.options;
1950
2363
  var centsSchema = z3.int().check(z3.gte(0));
1951
2364
  var metadataSchema = z3.record(z3.string(), z3.unknown());
1952
2365
  var customerNameSchema = z3.string().check(z3.maxLength(200));
@@ -2031,10 +2444,12 @@ function phoneField(name, raw, country) {
2031
2444
 
2032
2445
  // src/index.ts
2033
2446
  var import_client2 = require("@aranova/tracking-react/client");
2447
+ var import_client3 = require("@aranova/tracking-react/client");
2034
2448
  // Annotate the CommonJS export names for ESM import in node:
2035
2449
  0 && (module.exports = {
2036
2450
  AdPlatformTracking,
2037
2451
  AranovaApiError,
2452
+ BookingHandoffLink,
2038
2453
  ConsentBanner,
2039
2454
  DEFAULT_DECLINE_TTL_DAYS,
2040
2455
  DEFAULT_PHONE_COUNTRY,
@@ -2042,9 +2457,12 @@ var import_client2 = require("@aranova/tracking-react/client");
2042
2457
  GoogleAdsTracking,
2043
2458
  NAMED_RANGES,
2044
2459
  PhoneField,
2460
+ SALE_SOURCES,
2045
2461
  SUPPORTED_CURRENCIES,
2046
2462
  TRACKING_PARAM_KEYS,
2047
2463
  TRACKING_RANGES,
2464
+ bookableSlots,
2465
+ buildBookingHandoffUrl,
2048
2466
  captureTrackingParamsFromLocation,
2049
2467
  clearStashedUserData,
2050
2468
  createSalesClient,
@@ -2059,10 +2477,12 @@ var import_client2 = require("@aranova/tracking-react/client");
2059
2477
  formatPhone,
2060
2478
  formatPhoneAsTyped,
2061
2479
  fromMinor,
2480
+ getBookingIntegration,
2062
2481
  getConsentChoice,
2063
2482
  getConsentState,
2064
2483
  getEventSemantics,
2065
2484
  getTrackingConfigRuntime,
2485
+ newBookingAttemptKey,
2066
2486
  onConsentChange,
2067
2487
  optIn,
2068
2488
  optOut,
@@ -2077,9 +2497,14 @@ var import_client2 = require("@aranova/tracking-react/client");
2077
2497
  saleUpdateSchema,
2078
2498
  salesRequest,
2079
2499
  setConsentState,
2500
+ shopToday,
2080
2501
  stashUserData,
2081
2502
  toE164,
2082
2503
  toMinor,
2504
+ useBookingHandoffUrl,
2505
+ useBookingIntegration,
2506
+ useBookingSlots,
2507
+ useBookingSubmit,
2083
2508
  useConsent,
2084
2509
  useConsentState,
2085
2510
  useCookiePreferences,