@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/README.md +77 -0
- package/dist/{tracking-config-runtime-C6H7ofmo.d.ts → bookings-CqCuuyP6.d.mts} +165 -1
- package/dist/{tracking-config-runtime-C6H7ofmo.d.mts → bookings-DIu1ho6v.d.ts} +165 -1
- package/dist/client.d.mts +58 -5
- package/dist/client.d.ts +58 -5
- package/dist/client.js +287 -11
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +282 -11
- package/dist/client.mjs.map +1 -1
- package/dist/form-submit-BLb28aCk.d.mts +75 -0
- package/dist/form-submit-BLb28aCk.d.ts +75 -0
- package/dist/index.d.mts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +470 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +465 -4
- package/dist/index.mjs.map +1 -1
- package/dist/{ingest-typed-CUpdCtmM.d.ts → ingest-typed-D2S3Kb8m.d.ts} +83 -2
- package/dist/{ingest-typed-oAwnKqH2.d.mts → ingest-typed-D7UZr0EM.d.mts} +83 -2
- package/dist/{phone-utils-qLfVpQI7.d.mts → phone-utils-CM8eUPz8.d.mts} +2 -74
- package/dist/{phone-utils-qLfVpQI7.d.ts → phone-utils-l8ey3Zdm.d.ts} +2 -74
- package/dist/phone.d.mts +2 -1
- package/dist/phone.d.ts +2 -1
- package/dist/{sales-C3O_xxZ_.d.ts → sales-CxJVthx_.d.mts} +23 -2
- package/dist/{sales-BuKD_bdo.d.mts → sales-hlcVTJSS.d.ts} +23 -2
- package/dist/sales.d.mts +3 -2
- package/dist/sales.d.ts +3 -2
- package/dist/sales.js +386 -38
- package/dist/sales.js.map +1 -1
- package/dist/sales.mjs +384 -38
- package/dist/sales.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -221,6 +221,8 @@ var TRACKING_CAPABILITIES = [
|
|
|
221
221
|
"ad_tags_meta",
|
|
222
222
|
"base_tracking",
|
|
223
223
|
"blog_rendering",
|
|
224
|
+
"booking_handoff",
|
|
225
|
+
"booking_integration",
|
|
224
226
|
"calendar_read",
|
|
225
227
|
"calendar_write",
|
|
226
228
|
"consent_controls",
|
|
@@ -500,6 +502,42 @@ function buildLandingPayloadFields(sessionId, override) {
|
|
|
500
502
|
};
|
|
501
503
|
}
|
|
502
504
|
|
|
505
|
+
// ../tracking-core/src/uuid.ts
|
|
506
|
+
function uuidV4() {
|
|
507
|
+
const cryptoApi = globalThis.crypto;
|
|
508
|
+
if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
|
|
509
|
+
const bytes = new Uint8Array(16);
|
|
510
|
+
if (typeof cryptoApi?.getRandomValues === "function") cryptoApi.getRandomValues(bytes);
|
|
511
|
+
else for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
512
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
513
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
514
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
515
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ../tracking-core/src/session.ts
|
|
519
|
+
var SESSION_STORAGE_KEY = "aranova_tracking_session";
|
|
520
|
+
var SESSION_IDLE_MS = 30 * 60 * 1e3;
|
|
521
|
+
function readLocalStorage(key) {
|
|
522
|
+
try {
|
|
523
|
+
return window.localStorage.getItem(key);
|
|
524
|
+
} catch {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function peekSessionId(now = Date.now()) {
|
|
529
|
+
if (typeof window === "undefined") return null;
|
|
530
|
+
const raw = readLocalStorage(SESSION_STORAGE_KEY);
|
|
531
|
+
if (!raw) return null;
|
|
532
|
+
try {
|
|
533
|
+
const parsed = JSON.parse(raw);
|
|
534
|
+
if (typeof parsed.id !== "string" || typeof parsed.last_event_at !== "number") return null;
|
|
535
|
+
return now - parsed.last_event_at <= SESSION_IDLE_MS ? parsed.id : null;
|
|
536
|
+
} catch {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
503
541
|
// ../tracking-core/src/cross-site-link.ts
|
|
504
542
|
var LINK_PARAM = "_arv";
|
|
505
543
|
var LINK_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -628,6 +666,10 @@ function fireConversionWithConsent(input, options) {
|
|
|
628
666
|
}
|
|
629
667
|
|
|
630
668
|
// ../tracking-core/src/resources/conversion-config.ts
|
|
669
|
+
var BOOKING_MODES = ["live", "test", "simulated"];
|
|
670
|
+
function isBookingMode(value) {
|
|
671
|
+
return BOOKING_MODES.includes(value);
|
|
672
|
+
}
|
|
631
673
|
function isStringMap(value) {
|
|
632
674
|
return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
|
|
633
675
|
}
|
|
@@ -641,6 +683,70 @@ function parseFiring(value) {
|
|
|
641
683
|
currency: typeof f.currency === "string" ? f.currency : null
|
|
642
684
|
};
|
|
643
685
|
}
|
|
686
|
+
function isNumberList(value) {
|
|
687
|
+
return Array.isArray(value) && value.every((v) => typeof v === "number");
|
|
688
|
+
}
|
|
689
|
+
function isStringList(value) {
|
|
690
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
691
|
+
}
|
|
692
|
+
function parseWindowsMap(value) {
|
|
693
|
+
const windowsByKey = {};
|
|
694
|
+
if (!value || typeof value !== "object") return windowsByKey;
|
|
695
|
+
for (const [key, windows] of Object.entries(value)) {
|
|
696
|
+
if (!windows || typeof windows !== "object") continue;
|
|
697
|
+
const w = windows;
|
|
698
|
+
windowsByKey[key] = {
|
|
699
|
+
morning: isNumberList(w.morning) ? w.morning : [],
|
|
700
|
+
afternoon: isNumberList(w.afternoon) ? w.afternoon : []
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
return windowsByKey;
|
|
704
|
+
}
|
|
705
|
+
function parseBookingRules(value) {
|
|
706
|
+
if (!value || typeof value !== "object") return null;
|
|
707
|
+
const r = value;
|
|
708
|
+
if (typeof r.min_days_ahead !== "number" || typeof r.max_days_ahead !== "number") return null;
|
|
709
|
+
if (!r.hours || typeof r.hours !== "object") return null;
|
|
710
|
+
const hours = parseWindowsMap(r.hours);
|
|
711
|
+
return {
|
|
712
|
+
min_days_ahead: r.min_days_ahead,
|
|
713
|
+
max_days_ahead: r.max_days_ahead,
|
|
714
|
+
hours,
|
|
715
|
+
closed_dates: isStringList(r.closed_dates) ? r.closed_dates : [],
|
|
716
|
+
special_hours: parseWindowsMap(r.special_hours),
|
|
717
|
+
vehicle_required: r.vehicle_required === true,
|
|
718
|
+
drop_off_options: isStringList(r.drop_off_options) ? r.drop_off_options : []
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function parseBookingOption(value) {
|
|
722
|
+
if (!value || typeof value !== "object") return [];
|
|
723
|
+
const o = value;
|
|
724
|
+
if (typeof o.key !== "string" || !o.key || typeof o.label !== "string") return [];
|
|
725
|
+
return [{ key: o.key, label: o.label }];
|
|
726
|
+
}
|
|
727
|
+
function parseIntegrations(value) {
|
|
728
|
+
if (!value || typeof value !== "object") return {};
|
|
729
|
+
const autoleap = value.autoleap;
|
|
730
|
+
if (!autoleap || typeof autoleap !== "object") return {};
|
|
731
|
+
const booking = autoleap.booking;
|
|
732
|
+
if (!booking || typeof booking !== "object") return {};
|
|
733
|
+
const b = booking;
|
|
734
|
+
if (typeof b.fallback_url !== "string" || !b.fallback_url) return {};
|
|
735
|
+
return {
|
|
736
|
+
autoleap: {
|
|
737
|
+
booking: {
|
|
738
|
+
enabled: b.enabled === true,
|
|
739
|
+
fallback_url: b.fallback_url,
|
|
740
|
+
timezone: typeof b.timezone === "string" ? b.timezone : "UTC",
|
|
741
|
+
config_version: typeof b.config_version === "number" ? b.config_version : null,
|
|
742
|
+
accepts_bookings: b.accepts_bookings === true,
|
|
743
|
+
rules: parseBookingRules(b.rules),
|
|
744
|
+
options: Array.isArray(b.options) ? b.options.flatMap(parseBookingOption) : [],
|
|
745
|
+
...isBookingMode(b.mode) ? { mode: b.mode } : {}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
644
750
|
function parseTrigger(value) {
|
|
645
751
|
if (!value || typeof value !== "object") return null;
|
|
646
752
|
const t = value;
|
|
@@ -700,7 +806,8 @@ function parseConversionConfig(raw) {
|
|
|
700
806
|
meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
|
|
701
807
|
services,
|
|
702
808
|
goals,
|
|
703
|
-
linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : []
|
|
809
|
+
linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : [],
|
|
810
|
+
integrations: parseIntegrations(obj.integrations)
|
|
704
811
|
};
|
|
705
812
|
}
|
|
706
813
|
var CACHE_PREFIX = "_aranova_cfg_";
|
|
@@ -1147,6 +1254,13 @@ var TrackingConfigRuntime = class {
|
|
|
1147
1254
|
linkedHostnames() {
|
|
1148
1255
|
return this.current?.linked_hostnames ?? [];
|
|
1149
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Integration blocks, from the cached object even while unconfirmed: the booking fallback
|
|
1259
|
+
* link is safe to show stale. Same reference until the next adopt, for `useSyncExternalStore`.
|
|
1260
|
+
*/
|
|
1261
|
+
integrations() {
|
|
1262
|
+
return this.current?.integrations ?? null;
|
|
1263
|
+
}
|
|
1150
1264
|
async revalidateNow() {
|
|
1151
1265
|
try {
|
|
1152
1266
|
const headers = {};
|
|
@@ -1316,6 +1430,106 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
|
|
|
1316
1430
|
return runtime;
|
|
1317
1431
|
}
|
|
1318
1432
|
|
|
1433
|
+
// ../../contracts/autoleap-booking-links.json
|
|
1434
|
+
var autoleap_booking_links_default = {
|
|
1435
|
+
hosts: ["myalp.io"]
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
// ../tracking-core/src/integrations-public.ts
|
|
1439
|
+
var HANDOFF_HOSTS = new Set(autoleap_booking_links_default.hosts);
|
|
1440
|
+
function getBookingIntegration(integrations) {
|
|
1441
|
+
const booking = integrations?.autoleap?.booking;
|
|
1442
|
+
if (!booking) return null;
|
|
1443
|
+
const rules = booking.rules ? camelRules(booking.rules) : null;
|
|
1444
|
+
return {
|
|
1445
|
+
provider: "autoleap",
|
|
1446
|
+
enabled: booking.enabled,
|
|
1447
|
+
fallbackUrl: booking.fallback_url,
|
|
1448
|
+
acceptsBookings: booking.accepts_bookings && rules !== null && booking.config_version !== null,
|
|
1449
|
+
timezone: booking.timezone,
|
|
1450
|
+
configVersion: booking.config_version,
|
|
1451
|
+
rules,
|
|
1452
|
+
options: booking.options.map((option) => ({ key: option.key, label: option.label })),
|
|
1453
|
+
mode: booking.mode ?? "live"
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function camelRules(rules) {
|
|
1457
|
+
return {
|
|
1458
|
+
minDaysAhead: rules.min_days_ahead,
|
|
1459
|
+
maxDaysAhead: rules.max_days_ahead,
|
|
1460
|
+
hours: rules.hours,
|
|
1461
|
+
closedDates: rules.closed_dates,
|
|
1462
|
+
specialHours: rules.special_hours,
|
|
1463
|
+
vehicleRequired: rules.vehicle_required,
|
|
1464
|
+
dropOffOptions: rules.drop_off_options
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
var WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
1468
|
+
function shopToday(timeZone, now = /* @__PURE__ */ new Date()) {
|
|
1469
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
1470
|
+
timeZone,
|
|
1471
|
+
year: "numeric",
|
|
1472
|
+
month: "2-digit",
|
|
1473
|
+
day: "2-digit"
|
|
1474
|
+
}).formatToParts(now);
|
|
1475
|
+
const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
1476
|
+
return `${part("year")}-${part("month")}-${part("day")}`;
|
|
1477
|
+
}
|
|
1478
|
+
function addDays(isoDate, days) {
|
|
1479
|
+
const [year, month, day] = isoDate.split("-").map(Number);
|
|
1480
|
+
const date = new Date(Date.UTC(year, month - 1, day + days));
|
|
1481
|
+
return date.toISOString().slice(0, 10);
|
|
1482
|
+
}
|
|
1483
|
+
function bookableSlots(rules, { timeZone, now = /* @__PURE__ */ new Date() }) {
|
|
1484
|
+
const today = shopToday(timeZone, now);
|
|
1485
|
+
const days = [];
|
|
1486
|
+
const closed = new Set(rules.closedDates);
|
|
1487
|
+
for (let offset = rules.minDaysAhead; offset <= rules.maxDaysAhead; offset += 1) {
|
|
1488
|
+
const date = addDays(today, offset);
|
|
1489
|
+
if (closed.has(date)) continue;
|
|
1490
|
+
const weekday = WEEKDAYS[(/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay()];
|
|
1491
|
+
const windows = rules.specialHours[date] ?? rules.hours[weekday];
|
|
1492
|
+
if (!windows || windows.morning.length === 0 && windows.afternoon.length === 0) continue;
|
|
1493
|
+
days.push({
|
|
1494
|
+
date,
|
|
1495
|
+
weekday,
|
|
1496
|
+
morning: [...windows.morning],
|
|
1497
|
+
afternoon: [...windows.afternoon]
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
return days;
|
|
1501
|
+
}
|
|
1502
|
+
function buildBookingHandoffUrl(fallbackUrl, prefill = {}) {
|
|
1503
|
+
let url;
|
|
1504
|
+
try {
|
|
1505
|
+
url = new URL(fallbackUrl);
|
|
1506
|
+
} catch {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
if (url.protocol !== "https:" || !HANDOFF_HOSTS.has(url.hostname) || url.username || url.password || url.port) {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
const [firstName, lastName] = prefill.firstName?.trim() ? [prefill.firstName, prefill.lastName] : splitFullName(prefill.name);
|
|
1513
|
+
const values = [
|
|
1514
|
+
["firstName", firstName?.trim()],
|
|
1515
|
+
["lastName", lastName?.trim()],
|
|
1516
|
+
["phone", prefill.phone ? nationalDigits(prefill.phone) : ""],
|
|
1517
|
+
["email", prefill.email?.trim()]
|
|
1518
|
+
];
|
|
1519
|
+
for (const [key, value] of values) {
|
|
1520
|
+
if (value) url.searchParams.set(key, value);
|
|
1521
|
+
}
|
|
1522
|
+
return url.toString();
|
|
1523
|
+
}
|
|
1524
|
+
function splitFullName(name) {
|
|
1525
|
+
const [first = "", ...rest] = (name ?? "").trim().split(/\s+/);
|
|
1526
|
+
return [first, rest.join(" ")];
|
|
1527
|
+
}
|
|
1528
|
+
function nationalDigits(phone) {
|
|
1529
|
+
const parsed = parsePhone(phone);
|
|
1530
|
+
return (parsed.isValid ? parsed.national : phone).replace(/\D/g, "");
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1319
1533
|
// ../tracking-core/src/events/semantics.ts
|
|
1320
1534
|
var EVENT_SEMANTICS = {
|
|
1321
1535
|
page_view: {
|
|
@@ -1422,6 +1636,7 @@ var AranovaApiError = class extends Error {
|
|
|
1422
1636
|
this.status = options.status;
|
|
1423
1637
|
this.code = options.code;
|
|
1424
1638
|
this.requestId = options.requestId;
|
|
1639
|
+
this.context = options.context;
|
|
1425
1640
|
}
|
|
1426
1641
|
};
|
|
1427
1642
|
|
|
@@ -1448,19 +1663,24 @@ async function apiRequest(config, method, path, body, extraHeaders) {
|
|
|
1448
1663
|
if (!response.ok) {
|
|
1449
1664
|
let detail;
|
|
1450
1665
|
let code;
|
|
1666
|
+
let context;
|
|
1451
1667
|
try {
|
|
1452
1668
|
const parsed = await response.json();
|
|
1453
1669
|
if (parsed && typeof parsed === "object") {
|
|
1454
1670
|
const record3 = parsed;
|
|
1455
1671
|
if (typeof record3.detail === "string") detail = record3.detail;
|
|
1456
1672
|
if (typeof record3.code === "string") code = record3.code;
|
|
1673
|
+
if (record3.context && typeof record3.context === "object") {
|
|
1674
|
+
context = record3.context;
|
|
1675
|
+
}
|
|
1457
1676
|
}
|
|
1458
1677
|
} catch {
|
|
1459
1678
|
}
|
|
1460
1679
|
throw new AranovaApiError(detail ?? response.statusText ?? "Request failed", {
|
|
1461
1680
|
status: response.status,
|
|
1462
1681
|
code,
|
|
1463
|
-
requestId: response.headers.get("x-request-id") ?? void 0
|
|
1682
|
+
requestId: response.headers.get("x-request-id") ?? void 0,
|
|
1683
|
+
context
|
|
1464
1684
|
});
|
|
1465
1685
|
}
|
|
1466
1686
|
if (response.status === 204) return void 0;
|
|
@@ -1470,6 +1690,214 @@ async function apiRequest(config, method, path, body, extraHeaders) {
|
|
|
1470
1690
|
// ../tracking-core/src/resources/sales/transport.ts
|
|
1471
1691
|
var salesRequest = apiRequest;
|
|
1472
1692
|
|
|
1693
|
+
// ../tracking-core/src/resources/sales/bookings.ts
|
|
1694
|
+
var BOOKING_SIMULATE_HEADER = "X-Aranova-Simulate";
|
|
1695
|
+
var BOOKING_ERROR_CODES = {
|
|
1696
|
+
unavailable: "booking_unavailable",
|
|
1697
|
+
stale: "booking_config_stale",
|
|
1698
|
+
rejected: "booking_rejected"
|
|
1699
|
+
};
|
|
1700
|
+
var POLL_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
1701
|
+
var INVALID_DETAILS = "Please check your details and try again.";
|
|
1702
|
+
var newBookingAttemptKey = uuidV4;
|
|
1703
|
+
function sleep(ms) {
|
|
1704
|
+
return new Promise((resolve) => {
|
|
1705
|
+
setTimeout(resolve, ms);
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
function outcomeOf(body) {
|
|
1709
|
+
const mode = body.mode ?? "live";
|
|
1710
|
+
if (body.status === "created")
|
|
1711
|
+
return { status: "created", id: body.id, requestCode: body.request_code, mode };
|
|
1712
|
+
if (body.status === "pending") return { status: "pending", id: body.id, mode };
|
|
1713
|
+
return { status: "failed", id: body.id, fallbackUrl: body.fallback_url, mode };
|
|
1714
|
+
}
|
|
1715
|
+
function formSubmitPayload(input, formId, labels) {
|
|
1716
|
+
const vehicle = input.vehicle ? `${input.vehicle.year} ${input.vehicle.make} ${input.vehicle.model}` : "";
|
|
1717
|
+
const field = (name, type, label, value) => ({
|
|
1718
|
+
name,
|
|
1719
|
+
type,
|
|
1720
|
+
label,
|
|
1721
|
+
value
|
|
1722
|
+
});
|
|
1723
|
+
return {
|
|
1724
|
+
form: {
|
|
1725
|
+
id: formId,
|
|
1726
|
+
action: "/tracking/bookings",
|
|
1727
|
+
fields: [
|
|
1728
|
+
field("first_name", "text", "First name", input.firstName),
|
|
1729
|
+
field("last_name", "text", "Last name", input.lastName),
|
|
1730
|
+
// E.164 on the wire: it keys the customer and enhanced-conversion matching.
|
|
1731
|
+
field("phone", "tel", "Phone", toE164(input.phone) ?? input.phone),
|
|
1732
|
+
field("email", "email", "Email", input.email ?? ""),
|
|
1733
|
+
field(
|
|
1734
|
+
"service",
|
|
1735
|
+
"select",
|
|
1736
|
+
"Service",
|
|
1737
|
+
input.services.map((key) => labels.get(key) ?? key).join(", ")
|
|
1738
|
+
),
|
|
1739
|
+
field("vehicle", "text", "Vehicle", vehicle),
|
|
1740
|
+
field(
|
|
1741
|
+
"start",
|
|
1742
|
+
"text",
|
|
1743
|
+
"Requested time",
|
|
1744
|
+
`${input.startDate} ${String(input.startHour).padStart(2, "0")}:00`
|
|
1745
|
+
),
|
|
1746
|
+
field("note", "textarea", "Notes", input.notes ?? "")
|
|
1747
|
+
]
|
|
1748
|
+
},
|
|
1749
|
+
page: { path: typeof window === "undefined" ? "/" : window.location.pathname }
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
function runtimeOf(firing) {
|
|
1753
|
+
return firing && "integrations" in firing ? firing : null;
|
|
1754
|
+
}
|
|
1755
|
+
function isLocalUrl(url) {
|
|
1756
|
+
try {
|
|
1757
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname);
|
|
1758
|
+
} catch {
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function bookingSetupMismatch(endpoint, runtime) {
|
|
1763
|
+
if (!runtime || !isLocalUrl(endpoint)) return null;
|
|
1764
|
+
const configUrl = resolveTrackingConfigUrl(runtime.ref);
|
|
1765
|
+
if (isLocalUrl(configUrl)) return null;
|
|
1766
|
+
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.`;
|
|
1767
|
+
}
|
|
1768
|
+
function createSalesBookingsClient(config, firing) {
|
|
1769
|
+
const submittedForms = /* @__PURE__ */ new Set();
|
|
1770
|
+
const staleAttempts = /* @__PURE__ */ new Set();
|
|
1771
|
+
let isSetupChecked = false;
|
|
1772
|
+
function warnOnSetupMismatch(runtime) {
|
|
1773
|
+
if (isSetupChecked) return;
|
|
1774
|
+
isSetupChecked = true;
|
|
1775
|
+
const warning = bookingSetupMismatch(config.endpoint, runtime);
|
|
1776
|
+
if (warning) console.warn(warning);
|
|
1777
|
+
}
|
|
1778
|
+
function handoff(input) {
|
|
1779
|
+
const booking = getBookingIntegration(runtimeOf(firing)?.integrations());
|
|
1780
|
+
if (!booking) return null;
|
|
1781
|
+
return buildBookingHandoffUrl(booking.fallbackUrl, {
|
|
1782
|
+
firstName: input.firstName,
|
|
1783
|
+
lastName: input.lastName,
|
|
1784
|
+
phone: input.phone,
|
|
1785
|
+
email: input.email
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
function failedBeforeSend(input) {
|
|
1789
|
+
const mode = getBookingIntegration(runtimeOf(firing)?.integrations())?.mode ?? "live";
|
|
1790
|
+
return { status: "failed", id: null, fallbackUrl: handoff(input), mode };
|
|
1791
|
+
}
|
|
1792
|
+
function withHandoff(outcome, input) {
|
|
1793
|
+
if (outcome.status === "failed" && !outcome.fallbackUrl) {
|
|
1794
|
+
return { ...outcome, fallbackUrl: handoff(input) };
|
|
1795
|
+
}
|
|
1796
|
+
return outcome;
|
|
1797
|
+
}
|
|
1798
|
+
async function poll(id, mode) {
|
|
1799
|
+
let outcome = { status: "pending", id, mode };
|
|
1800
|
+
for (const delay of POLL_DELAYS_MS) {
|
|
1801
|
+
await sleep(delay);
|
|
1802
|
+
try {
|
|
1803
|
+
const path = `/bookings/${encodeURIComponent(id)}`;
|
|
1804
|
+
outcome = outcomeOf(await salesRequest(config, "GET", path));
|
|
1805
|
+
} catch {
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
if (outcome.status !== "pending") return outcome;
|
|
1809
|
+
}
|
|
1810
|
+
return outcome;
|
|
1811
|
+
}
|
|
1812
|
+
async function rejected(error, input, attemptKey, runtime) {
|
|
1813
|
+
if (!(error instanceof AranovaApiError)) {
|
|
1814
|
+
return failedBeforeSend(input);
|
|
1815
|
+
}
|
|
1816
|
+
if (error.code === BOOKING_ERROR_CODES.stale) {
|
|
1817
|
+
if (staleAttempts.has(attemptKey)) return failedBeforeSend(input);
|
|
1818
|
+
staleAttempts.add(attemptKey);
|
|
1819
|
+
await runtime?.revalidate();
|
|
1820
|
+
return { status: "stale", message: error.message };
|
|
1821
|
+
}
|
|
1822
|
+
if (error.code === BOOKING_ERROR_CODES.rejected) {
|
|
1823
|
+
const reasons = error.context?.reasons;
|
|
1824
|
+
return {
|
|
1825
|
+
status: "invalid",
|
|
1826
|
+
message: error.message,
|
|
1827
|
+
reasons: Array.isArray(reasons) ? reasons.map(String) : []
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
if (error.code === "validation_error") {
|
|
1831
|
+
return { status: "invalid", message: INVALID_DETAILS, reasons: [] };
|
|
1832
|
+
}
|
|
1833
|
+
return failedBeforeSend(input);
|
|
1834
|
+
}
|
|
1835
|
+
return {
|
|
1836
|
+
async submit(input, { attemptKey, formSubmit, simulate }) {
|
|
1837
|
+
registerCapability("booking_integration");
|
|
1838
|
+
const runtime = runtimeOf(firing);
|
|
1839
|
+
warnOnSetupMismatch(runtime);
|
|
1840
|
+
const booking = getBookingIntegration(runtime?.integrations());
|
|
1841
|
+
if (!booking?.acceptsBookings || booking.configVersion === null) {
|
|
1842
|
+
return failedBeforeSend(input);
|
|
1843
|
+
}
|
|
1844
|
+
if (formSubmit && !submittedForms.has(attemptKey)) {
|
|
1845
|
+
submittedForms.add(attemptKey);
|
|
1846
|
+
const labels = new Map(booking.options.map((option) => [option.key, option.label]));
|
|
1847
|
+
formSubmit.tracking.trackEvent(
|
|
1848
|
+
"form_submit",
|
|
1849
|
+
formSubmitPayload(input, formSubmit.formId, labels)
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
const body = {
|
|
1853
|
+
idempotency_key: attemptKey,
|
|
1854
|
+
// The tracking client's own visit, so the booking and its form_submit share one session.
|
|
1855
|
+
session_id: formSubmit?.tracking.getSessionId() ?? peekSessionId() ?? `no-session-${attemptKey}`,
|
|
1856
|
+
config_version: booking.configVersion,
|
|
1857
|
+
start_date: input.startDate,
|
|
1858
|
+
start_hour: input.startHour,
|
|
1859
|
+
first_name: input.firstName.trim(),
|
|
1860
|
+
last_name: input.lastName.trim(),
|
|
1861
|
+
phone: input.phone,
|
|
1862
|
+
email: input.email?.trim() || null,
|
|
1863
|
+
vehicle: input.vehicle ?? null,
|
|
1864
|
+
drop_off: input.dropOff ?? null,
|
|
1865
|
+
services: input.services,
|
|
1866
|
+
notes: input.notes?.trim() || null,
|
|
1867
|
+
marketing_sms_consent: input.marketingSmsConsent === true,
|
|
1868
|
+
page_path: typeof window === "undefined" ? null : window.location.pathname
|
|
1869
|
+
};
|
|
1870
|
+
let response;
|
|
1871
|
+
try {
|
|
1872
|
+
const headers = simulate ? { [BOOKING_SIMULATE_HEADER]: simulate } : void 0;
|
|
1873
|
+
response = await salesRequest(
|
|
1874
|
+
config,
|
|
1875
|
+
"POST",
|
|
1876
|
+
"/bookings",
|
|
1877
|
+
body,
|
|
1878
|
+
headers
|
|
1879
|
+
);
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
return rejected(error, input, attemptKey, runtime);
|
|
1882
|
+
}
|
|
1883
|
+
const outcome = outcomeOf(response);
|
|
1884
|
+
return withHandoff(
|
|
1885
|
+
outcome.status === "pending" ? await poll(outcome.id, outcome.mode) : outcome,
|
|
1886
|
+
input
|
|
1887
|
+
);
|
|
1888
|
+
},
|
|
1889
|
+
async status(id) {
|
|
1890
|
+
return outcomeOf(
|
|
1891
|
+
await salesRequest(
|
|
1892
|
+
config,
|
|
1893
|
+
"GET",
|
|
1894
|
+
`/bookings/${encodeURIComponent(id)}`
|
|
1895
|
+
)
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1473
1901
|
// ../tracking-core/src/resources/sales/client.ts
|
|
1474
1902
|
function fireRecordedConversions(firing, input, recorded, sale, currency) {
|
|
1475
1903
|
if (!firing) return;
|
|
@@ -1644,7 +2072,8 @@ function createSalesClient(config) {
|
|
|
1644
2072
|
async config() {
|
|
1645
2073
|
return salesRequest(config, "GET", "/business/config");
|
|
1646
2074
|
}
|
|
1647
|
-
}
|
|
2075
|
+
},
|
|
2076
|
+
bookings: createSalesBookingsClient(config, config.firing)
|
|
1648
2077
|
};
|
|
1649
2078
|
}
|
|
1650
2079
|
|
|
@@ -1653,6 +2082,12 @@ import * as z3 from "zod/mini";
|
|
|
1653
2082
|
|
|
1654
2083
|
// ../tracking-core/src/resources/sales/generated/zod.gen.ts
|
|
1655
2084
|
import * as z2 from "zod/mini";
|
|
2085
|
+
var zAutoLeapSaleFacts = z2.object({
|
|
2086
|
+
first_order: z2.boolean(),
|
|
2087
|
+
fleet_customer: z2.boolean(),
|
|
2088
|
+
invoice_code: z2.string(),
|
|
2089
|
+
invoice_id: z2.string()
|
|
2090
|
+
});
|
|
1656
2091
|
var zErrorCode = z2.enum([
|
|
1657
2092
|
"missing_credentials",
|
|
1658
2093
|
"invalid_api_key",
|
|
@@ -1680,6 +2115,9 @@ var zErrorCode = z2.enum([
|
|
|
1680
2115
|
"booking_not_reschedulable",
|
|
1681
2116
|
"manage_token_invalid",
|
|
1682
2117
|
"google_error",
|
|
2118
|
+
"booking_unavailable",
|
|
2119
|
+
"booking_config_stale",
|
|
2120
|
+
"booking_rejected",
|
|
1683
2121
|
"calendar_not_owned",
|
|
1684
2122
|
"calendar_is_sandbox",
|
|
1685
2123
|
"sandbox_exists",
|
|
@@ -1721,6 +2159,7 @@ var zSaleServiceInputSchema = z2.strictObject({
|
|
|
1721
2159
|
amount_cents: z2.int().check(z2.gte(0)),
|
|
1722
2160
|
service: z2.string()
|
|
1723
2161
|
});
|
|
2162
|
+
var zSaleSource = z2.enum(["api", "admin", "autoleap"]);
|
|
1724
2163
|
var zSmsAutomationState = z2.enum([
|
|
1725
2164
|
"needs_check",
|
|
1726
2165
|
"failed",
|
|
@@ -1752,7 +2191,8 @@ var zSmsWithheldReason = z2.enum([
|
|
|
1752
2191
|
"number_not_active",
|
|
1753
2192
|
"a2p_registration_required",
|
|
1754
2193
|
"alert_recipient_cannot_receive_sms",
|
|
1755
|
-
"line_type_unresolved"
|
|
2194
|
+
"line_type_unresolved",
|
|
2195
|
+
"sale_source_sends_own_messages"
|
|
1756
2196
|
]);
|
|
1757
2197
|
var zSaleSmsStatusSchema = z2.object({
|
|
1758
2198
|
can_send_now: z2._default(z2.boolean(), false),
|
|
@@ -1861,6 +2301,8 @@ var zSaleEntitySchema = z2.object({
|
|
|
1861
2301
|
sms_automation: z2.nullable(zSaleSmsStatusSchema),
|
|
1862
2302
|
sms_consent: z2._default(z2.boolean(), false),
|
|
1863
2303
|
sms_consent_granted_at: z2.nullable(z2.iso.datetime({ offset: true })),
|
|
2304
|
+
source: zSaleSource,
|
|
2305
|
+
source_facts: z2.nullable(zAutoLeapSaleFacts),
|
|
1864
2306
|
updated_at: z2.iso.datetime({ offset: true }),
|
|
1865
2307
|
writeback: z2.nullable(zSaleWritebackStatusSchema)
|
|
1866
2308
|
});
|
|
@@ -1870,6 +2312,7 @@ var saleItemSchema = zSaleItemInputSchema;
|
|
|
1870
2312
|
var saleServiceSchema = zSaleServiceInputSchema;
|
|
1871
2313
|
var currencySchema = zSupportedCurrency;
|
|
1872
2314
|
var SUPPORTED_CURRENCIES = currencySchema.options;
|
|
2315
|
+
var SALE_SOURCES = zSaleSource.options;
|
|
1873
2316
|
var centsSchema = z3.int().check(z3.gte(0));
|
|
1874
2317
|
var metadataSchema = z3.record(z3.string(), z3.unknown());
|
|
1875
2318
|
var customerNameSchema = z3.string().check(z3.maxLength(200));
|
|
@@ -1958,9 +2401,17 @@ import {
|
|
|
1958
2401
|
usePhoneConfig,
|
|
1959
2402
|
usePhoneField
|
|
1960
2403
|
} from "@aranova/tracking-react/client";
|
|
2404
|
+
import {
|
|
2405
|
+
BookingHandoffLink,
|
|
2406
|
+
useBookingHandoffUrl,
|
|
2407
|
+
useBookingIntegration,
|
|
2408
|
+
useBookingSlots,
|
|
2409
|
+
useBookingSubmit
|
|
2410
|
+
} from "@aranova/tracking-react/client";
|
|
1961
2411
|
export {
|
|
1962
2412
|
AdPlatformTracking,
|
|
1963
2413
|
AranovaApiError,
|
|
2414
|
+
BookingHandoffLink,
|
|
1964
2415
|
ConsentBanner,
|
|
1965
2416
|
DEFAULT_DECLINE_TTL_DAYS,
|
|
1966
2417
|
DEFAULT_PHONE_COUNTRY,
|
|
@@ -1968,9 +2419,12 @@ export {
|
|
|
1968
2419
|
GoogleAdsTracking,
|
|
1969
2420
|
NAMED_RANGES,
|
|
1970
2421
|
PhoneField,
|
|
2422
|
+
SALE_SOURCES,
|
|
1971
2423
|
SUPPORTED_CURRENCIES,
|
|
1972
2424
|
TRACKING_PARAM_KEYS,
|
|
1973
2425
|
TRACKING_RANGES,
|
|
2426
|
+
bookableSlots,
|
|
2427
|
+
buildBookingHandoffUrl,
|
|
1974
2428
|
captureTrackingParamsFromLocation,
|
|
1975
2429
|
clearStashedUserData,
|
|
1976
2430
|
createSalesClient,
|
|
@@ -1985,10 +2439,12 @@ export {
|
|
|
1985
2439
|
formatPhone,
|
|
1986
2440
|
formatPhoneAsTyped,
|
|
1987
2441
|
fromMinor,
|
|
2442
|
+
getBookingIntegration,
|
|
1988
2443
|
getConsentChoice,
|
|
1989
2444
|
getConsentState,
|
|
1990
2445
|
getEventSemantics,
|
|
1991
2446
|
getTrackingConfigRuntime,
|
|
2447
|
+
newBookingAttemptKey,
|
|
1992
2448
|
onConsentChange,
|
|
1993
2449
|
optIn,
|
|
1994
2450
|
optOut,
|
|
@@ -2003,9 +2459,14 @@ export {
|
|
|
2003
2459
|
saleUpdateSchema,
|
|
2004
2460
|
salesRequest,
|
|
2005
2461
|
setConsentState,
|
|
2462
|
+
shopToday,
|
|
2006
2463
|
stashUserData,
|
|
2007
2464
|
toE164,
|
|
2008
2465
|
toMinor,
|
|
2466
|
+
useBookingHandoffUrl,
|
|
2467
|
+
useBookingIntegration,
|
|
2468
|
+
useBookingSlots,
|
|
2469
|
+
useBookingSubmit,
|
|
2009
2470
|
useConsent,
|
|
2010
2471
|
useConsentState,
|
|
2011
2472
|
useCookiePreferences,
|