@aranova/tracking-next 0.20.2 → 0.22.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.
Files changed (48) hide show
  1. package/README.md +80 -1
  2. package/dist/blog-server.js +12 -0
  3. package/dist/blog-server.js.map +1 -1
  4. package/dist/blog-server.mjs +12 -0
  5. package/dist/blog-server.mjs.map +1 -1
  6. package/dist/blog.js +7 -0
  7. package/dist/blog.js.map +1 -1
  8. package/dist/blog.mjs +7 -0
  9. package/dist/blog.mjs.map +1 -1
  10. package/dist/calendar-server.d.mts +53 -0
  11. package/dist/calendar-server.d.ts +53 -0
  12. package/dist/calendar-server.js +206 -0
  13. package/dist/calendar-server.js.map +1 -0
  14. package/dist/calendar-server.mjs +180 -0
  15. package/dist/calendar-server.mjs.map +1 -0
  16. package/dist/calendar.d.mts +112 -0
  17. package/dist/calendar.d.ts +112 -0
  18. package/dist/calendar.js +524 -0
  19. package/dist/calendar.js.map +1 -0
  20. package/dist/calendar.mjs +464 -0
  21. package/dist/calendar.mjs.map +1 -0
  22. package/dist/date-BHetnL7s.d.mts +26 -0
  23. package/dist/date-BHetnL7s.d.ts +26 -0
  24. package/dist/index.d.mts +62 -5
  25. package/dist/index.d.ts +62 -5
  26. package/dist/index.js +566 -21
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +529 -21
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/{phone-utils-B2vHJkNz.d.mts → phone-utils-BhWfNuPS.d.mts} +8 -0
  31. package/dist/{phone-utils-B2vHJkNz.d.ts → phone-utils-BhWfNuPS.d.ts} +8 -0
  32. package/dist/phone.d.mts +1 -1
  33. package/dist/phone.d.ts +1 -1
  34. package/dist/request-Doq36Rt0.d.mts +19 -0
  35. package/dist/request-Doq36Rt0.d.ts +19 -0
  36. package/dist/{sales-DrH6dY5F.d.mts → sales-C7tPnqbq.d.ts} +9 -48
  37. package/dist/{sales-DrH6dY5F.d.ts → sales-H_6bHrL-.d.mts} +9 -48
  38. package/dist/sales.d.mts +3 -1
  39. package/dist/sales.d.ts +3 -1
  40. package/dist/sales.js +29 -17
  41. package/dist/sales.js.map +1 -1
  42. package/dist/sales.mjs +29 -17
  43. package/dist/sales.mjs.map +1 -1
  44. package/dist/server.d.mts +1 -1
  45. package/dist/server.d.ts +1 -1
  46. package/dist/transport-C-_rFSR4.d.ts +721 -0
  47. package/dist/transport-yUF5Wize.d.mts +721 -0
  48. package/package.json +21 -1
package/dist/index.mjs CHANGED
@@ -258,6 +258,43 @@ var aranovaConsent = (function () {
258
258
  `.trim();
259
259
  }
260
260
 
261
+ // ../tracking-core/src/capabilities.ts
262
+ var TRACKING_CAPABILITIES = [
263
+ "ad_tags_google",
264
+ "ad_tags_meta",
265
+ "base_tracking",
266
+ "blog_rendering",
267
+ "calendar_read",
268
+ "calendar_write",
269
+ "consent_controls",
270
+ "conversion_goals_auto",
271
+ "conversion_goals_manual",
272
+ "cta_click_capture",
273
+ "form_capture",
274
+ "phone_click_capture",
275
+ "phone_fields"
276
+ ];
277
+ var CAPABILITY_DOM_ATTRIBUTE = "data-aranova-capability";
278
+ var registered = /* @__PURE__ */ new Set();
279
+ function registerCapability(capability) {
280
+ registered.add(capability);
281
+ }
282
+ function getRegisteredCapabilities() {
283
+ const all = new Set(registered);
284
+ for (const marker of readDomMarkers()) all.add(marker);
285
+ return Array.from(all).sort();
286
+ }
287
+ function readDomMarkers() {
288
+ if (typeof document === "undefined") return [];
289
+ const known = new Set(TRACKING_CAPABILITIES);
290
+ const found = [];
291
+ for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {
292
+ const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);
293
+ if (value && known.has(value)) found.push(value);
294
+ }
295
+ return found;
296
+ }
297
+
261
298
  // ../tracking-core/src/tracking.ts
262
299
  var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
263
300
  var TRACKING_PARAM_KEYS = [
@@ -602,7 +639,8 @@ function createTrackingClientContext(surface, input = {}) {
602
639
  page_title: input.pageTitle ?? (typeof document === "undefined" ? null : document.title || null),
603
640
  referrer: input.referrer ?? (typeof document === "undefined" ? null : document.referrer || null),
604
641
  environment: input.environment ?? "production",
605
- active_gtag_ids: input.activeGtagIds ?? null
642
+ active_gtag_ids: input.activeGtagIds ?? null,
643
+ capabilities: getRegisteredCapabilities()
606
644
  };
607
645
  }
608
646
  function createTrackingSessionUpsertPayload(trackingParams, input, context) {
@@ -946,6 +984,21 @@ function automaticThresholdMet(goal, eventType, metadata) {
946
984
  }
947
985
  }
948
986
 
987
+ // ../tracking-core/src/resources/intl/date.ts
988
+ function formatDateInTz(iso, timeZone, opts, locale) {
989
+ const date = new Date(iso);
990
+ if (Number.isNaN(date.getTime())) return iso;
991
+ return new Intl.DateTimeFormat(locale, {
992
+ year: "numeric",
993
+ month: "short",
994
+ day: "2-digit",
995
+ hour: "2-digit",
996
+ minute: "2-digit",
997
+ ...opts,
998
+ timeZone
999
+ }).format(date);
1000
+ }
1001
+
949
1002
  // ../tracking-core/src/resources/sales/money.ts
950
1003
  var MINOR_UNIT_EXPONENT = {
951
1004
  USD: 2,
@@ -965,19 +1018,6 @@ function formatMoney(cents, currency, locale) {
965
1018
  fromMinor(cents, currency)
966
1019
  );
967
1020
  }
968
- function formatDateInTz(iso, timeZone, opts, locale) {
969
- const date = new Date(iso);
970
- if (Number.isNaN(date.getTime())) return iso;
971
- return new Intl.DateTimeFormat(locale, {
972
- year: "numeric",
973
- month: "short",
974
- day: "2-digit",
975
- hour: "2-digit",
976
- minute: "2-digit",
977
- ...opts,
978
- timeZone
979
- }).format(date);
980
- }
981
1021
 
982
1022
  // ../tracking-core/src/resources/tracking-config-runtime.ts
983
1023
  var CACHE_PREFIX2 = "_aranova_cfg_runtime_";
@@ -1594,7 +1634,10 @@ function buildContext(surface, sdkVersion, packageName, environment, activeGtagI
1594
1634
  page_title: typeof document === "undefined" ? null : document.title || null,
1595
1635
  referrer: typeof document === "undefined" ? null : document.referrer || null,
1596
1636
  environment,
1597
- active_gtag_ids: activeGtagIds
1637
+ active_gtag_ids: activeGtagIds,
1638
+ // Rebuilt per flush (this runs inside the payload builder), so a client
1639
+ // constructed later on a deeper route still gets reported.
1640
+ capabilities: getRegisteredCapabilities()
1598
1641
  };
1599
1642
  }
1600
1643
  function readTrackingParams() {
@@ -2187,8 +2230,9 @@ function getEventDefinition(name) {
2187
2230
  }
2188
2231
 
2189
2232
  // ../tracking-core/src/ingest-typed.ts
2190
- function createTypedClient(raw, _registry, options = {}) {
2233
+ function createTypedClient(raw, registry, options = {}) {
2191
2234
  const debug = options.debug ?? false;
2235
+ if (registry.manual?.form_submit) registerCapability("form_capture");
2192
2236
  return {
2193
2237
  trackEvent(eventType, metadata, opts) {
2194
2238
  if (debug) {
@@ -2595,6 +2639,7 @@ function attachCtaClickCapture(client, config) {
2595
2639
  return () => {
2596
2640
  };
2597
2641
  }
2642
+ registerCapability("cta_click_capture");
2598
2643
  const selector = autoCapture.selector ?? DEFAULT_CTA_SELECTOR;
2599
2644
  function onClick(event) {
2600
2645
  const target = event.target;
@@ -2651,6 +2696,7 @@ function attachPhoneClickCapture(client, config) {
2651
2696
  return () => {
2652
2697
  };
2653
2698
  }
2699
+ registerCapability("phone_click_capture");
2654
2700
  const selector = autoCapture.selector ?? DEFAULT_TEL_SELECTOR;
2655
2701
  function onClick(event) {
2656
2702
  const target = event.target;
@@ -2680,7 +2726,7 @@ function attachPhoneClickCapture(client, config) {
2680
2726
  };
2681
2727
  }
2682
2728
 
2683
- // ../tracking-core/src/resources/sales/errors.ts
2729
+ // ../tracking-core/src/resources/http/errors.ts
2684
2730
  var AranovaApiError = class extends Error {
2685
2731
  constructor(message, options) {
2686
2732
  super(message);
@@ -2691,7 +2737,7 @@ var AranovaApiError = class extends Error {
2691
2737
  }
2692
2738
  };
2693
2739
 
2694
- // ../tracking-core/src/resources/sales/transport.ts
2740
+ // ../tracking-core/src/resources/http/request.ts
2695
2741
  function identityHeaders(config) {
2696
2742
  const headers = { [API_KEY_HEADER]: config.apiKey };
2697
2743
  if (config.sdkVersion) headers[SDK_VERSION_HEADER] = config.sdkVersion;
@@ -2703,8 +2749,8 @@ function identityHeaders(config) {
2703
2749
  function joinUrl(endpoint, path) {
2704
2750
  return `${endpoint.replace(/\/$/, "")}${path}`;
2705
2751
  }
2706
- async function salesRequest(config, method, path, body) {
2707
- const headers = identityHeaders(config);
2752
+ async function apiRequest(config, method, path, body, extraHeaders) {
2753
+ const headers = { ...identityHeaders(config), ...extraHeaders };
2708
2754
  if (body !== void 0) headers["Content-Type"] = "application/json";
2709
2755
  const response = await fetch(joinUrl(config.endpoint, path), {
2710
2756
  method,
@@ -2733,6 +2779,9 @@ async function salesRequest(config, method, path, body) {
2733
2779
  return await response.json();
2734
2780
  }
2735
2781
 
2782
+ // ../tracking-core/src/resources/sales/transport.ts
2783
+ var salesRequest = apiRequest;
2784
+
2736
2785
  // ../tracking-core/src/resources/sales/client.ts
2737
2786
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
2738
2787
  if (!firing) return;
@@ -2764,6 +2813,7 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2764
2813
  }
2765
2814
  }
2766
2815
  function createSalesClient(config) {
2816
+ if (config.firing) registerCapability("conversion_goals_manual");
2767
2817
  async function record(input) {
2768
2818
  const currency = input.currency ?? config.defaultCurrency;
2769
2819
  if (!currency) {
@@ -3063,6 +3113,7 @@ var DEFAULT_CHOICE2 = {
3063
3113
  expiresAt: null
3064
3114
  };
3065
3115
  function useCookiePreferences(options) {
3116
+ registerCapability("consent_controls");
3066
3117
  const [choice, setChoice] = useState(DEFAULT_CHOICE2);
3067
3118
  const ttlDays = options?.declineTtlDays;
3068
3119
  useEffect(() => {
@@ -3330,6 +3381,8 @@ function AdPlatformTracking({
3330
3381
  }, [trackingConfigKey, standalonePageView]);
3331
3382
  const gtagAll = trackingConfig ? [] : gtagIds ? Object.values(gtagIds).filter((id) => id != null) : gtagId ? [gtagId] : [];
3332
3383
  const metaAll = metaPixelIds ? Object.values(metaPixelIds).filter((id) => id != null) : metaPixelId ? [metaPixelId] : [];
3384
+ if (trackingConfig || gtagAll.length > 0) registerCapability("ad_tags_google");
3385
+ if (metaAll.length > 0) registerCapability("ad_tags_meta");
3333
3386
  const gtagLoaderId = gtagAll[0];
3334
3387
  const gtagInitScript = gtagAll.length === 1 ? createGtagInitScript(gtagAll[0]) : createGtagInitScriptMulti(gtagAll);
3335
3388
  const metaInitScript = metaAll.length === 1 ? createMetaPixelInitScript(metaAll[0]) : createMetaPixelInitScriptMulti(metaAll);
@@ -3378,7 +3431,7 @@ function GoogleAdsTracking(props) {
3378
3431
  import { usePathname, useSearchParams } from "next/navigation";
3379
3432
 
3380
3433
  // package.json
3381
- var version = "0.20.2";
3434
+ var version = "0.22.0";
3382
3435
 
3383
3436
  // src/factory.tsx
3384
3437
  import {
@@ -3420,6 +3473,7 @@ function usePhoneConfig() {
3420
3473
  };
3421
3474
  }
3422
3475
  function usePhoneField(opts = {}) {
3476
+ registerCapability("phone_fields");
3423
3477
  const cfg = usePhoneConfig();
3424
3478
  const country = opts.country ?? cfg.defaultCountry;
3425
3479
  const display = opts.display ?? cfg.display;
@@ -3506,6 +3560,8 @@ function createTracking(options) {
3506
3560
  conversionConfig,
3507
3561
  trackingConfig
3508
3562
  } = options;
3563
+ registerCapability("base_tracking");
3564
+ if (trackingConfig) registerCapability("conversion_goals_auto");
3509
3565
  if (!apiKey || !endpoint) {
3510
3566
  if (apiKey || endpoint) {
3511
3567
  console.warn(
@@ -3674,42 +3730,489 @@ function createTracking(options) {
3674
3730
  }
3675
3731
  return { TrackingProvider, useTracking };
3676
3732
  }
3733
+
3734
+ // ../tracking-core/src/resources/calendar/transport.ts
3735
+ async function calendarRequest(config, method, path, body) {
3736
+ return apiRequest(config, method, `/calendar${path}`, body);
3737
+ }
3738
+
3739
+ // ../tracking-core/src/resources/calendar/time.ts
3740
+ function resolveBrowserTimeZone() {
3741
+ try {
3742
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
3743
+ } catch {
3744
+ return "UTC";
3745
+ }
3746
+ }
3747
+ function partsInTz(date, timeZone) {
3748
+ const formatter = new Intl.DateTimeFormat("en-US", {
3749
+ timeZone,
3750
+ hour12: false,
3751
+ year: "numeric",
3752
+ month: "2-digit",
3753
+ day: "2-digit",
3754
+ hour: "2-digit",
3755
+ minute: "2-digit",
3756
+ second: "2-digit"
3757
+ });
3758
+ const out = {};
3759
+ for (const part of formatter.formatToParts(date)) {
3760
+ if (part.type !== "literal") out[part.type] = Number(part.value);
3761
+ }
3762
+ if (out.hour === 24) out.hour = 0;
3763
+ return out;
3764
+ }
3765
+ function offsetMinutesAt(date, timeZone) {
3766
+ const p = partsInTz(date, timeZone);
3767
+ const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
3768
+ return (asUtc - Math.floor(date.getTime() / 1e3) * 1e3) / 6e4;
3769
+ }
3770
+ function instantFromWallClock(timeZone, year, month, day, hour = 0, minute = 0) {
3771
+ const naive = Date.UTC(year, month - 1, day, hour, minute);
3772
+ const firstGuess = new Date(naive - offsetMinutesAt(new Date(naive), timeZone) * 6e4);
3773
+ const corrected = naive - offsetMinutesAt(firstGuess, timeZone) * 6e4;
3774
+ return new Date(corrected);
3775
+ }
3776
+ function localDateKey(iso, timeZone) {
3777
+ const p = partsInTz(new Date(iso), timeZone);
3778
+ const pad = (n) => String(n).padStart(2, "0");
3779
+ return `${p.year}-${pad(p.month)}-${pad(p.day)}`;
3780
+ }
3781
+ function startOfDayInTz(dateKey, timeZone) {
3782
+ const [year, month, day] = dateKey.split("-").map(Number);
3783
+ return instantFromWallClock(timeZone, year, month, day, 0, 0);
3784
+ }
3785
+ function endOfDayInTz(dateKey, timeZone) {
3786
+ const start = startOfDayInTz(dateKey, timeZone);
3787
+ const p = partsInTz(start, timeZone);
3788
+ return instantFromWallClock(timeZone, p.year, p.month, p.day + 1, 0, 0);
3789
+ }
3790
+ function durationMinutes(startIso, endIso) {
3791
+ return Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 6e4);
3792
+ }
3793
+ function formatSlotRange(startIso, endIso, timeZone, locale) {
3794
+ const opts = { hour: "2-digit", minute: "2-digit", timeZone };
3795
+ const fmt = new Intl.DateTimeFormat(locale, opts);
3796
+ return `${fmt.format(new Date(startIso))} \u2013 ${fmt.format(new Date(endIso))}`;
3797
+ }
3798
+
3799
+ // ../tracking-core/src/resources/calendar/read-client.ts
3800
+ function withBusiness(config, path) {
3801
+ if (!config.businessId) return path;
3802
+ const separator = path.includes("?") ? "&" : "?";
3803
+ return `${path}${separator}business_id=${encodeURIComponent(config.businessId)}`;
3804
+ }
3805
+ function createCalendarReadClient(config) {
3806
+ registerCapability("calendar_read");
3807
+ return {
3808
+ async listCalendars() {
3809
+ return calendarRequest(config, "GET", withBusiness(config, "/calendars"));
3810
+ },
3811
+ async busy(calendarKey, query) {
3812
+ const params = new URLSearchParams({
3813
+ since: query.since,
3814
+ until: query.until
3815
+ });
3816
+ const timeZone = query.timeZone ?? config.defaultTimeZone ?? resolveBrowserTimeZone();
3817
+ params.set("time_zone", timeZone);
3818
+ return calendarRequest(
3819
+ config,
3820
+ "GET",
3821
+ withBusiness(config, `/${encodeURIComponent(calendarKey)}/busy?${params.toString()}`)
3822
+ );
3823
+ }
3824
+ };
3825
+ }
3826
+ function isCalendarError(error, code) {
3827
+ if (!(error instanceof AranovaApiError)) return false;
3828
+ return code === void 0 || error.code === code;
3829
+ }
3830
+
3831
+ // ../tracking-core/src/resources/calendar/schema.ts
3832
+ import { z as z13 } from "zod";
3833
+ var CALENDAR_METADATA_SCHEMA_VERSION = 1;
3834
+ var MAX_ATTENDEES = 20;
3835
+ var MAX_REMINDER_OVERRIDES = 5;
3836
+ var MAX_REMINDER_MINUTES = 40320;
3837
+ var MAX_BOOKING_MINUTES = 1440;
3838
+ var BOOKING_SOURCES = ["website", "phone", "walk_in", "import", "operator"];
3839
+ var BOOKING_STATUSES = ["pending", "confirmed", "cancelled", "no_show"];
3840
+ var GOOGLE_SYNC_STATES = ["queued", "synced", "uncertain", "failed", "drifted"];
3841
+ var CALENDAR_SYNC_STATUSES = [
3842
+ "pending_invite",
3843
+ "awaiting_calendar_selection",
3844
+ "connected",
3845
+ "needs_reconnect",
3846
+ "revoked",
3847
+ "disabled"
3848
+ ];
3849
+ var CALENDAR_ERROR_CODES = [
3850
+ "slot_unavailable",
3851
+ "calendar_not_connected",
3852
+ "calendar_disconnected",
3853
+ "booking_not_found",
3854
+ "outside_booking_window",
3855
+ "rate_limited",
3856
+ "google_error"
3857
+ ];
3858
+ var isoInstant = z13.string().datetime({ offset: true });
3859
+ function isKnownTimeZone(timeZone) {
3860
+ try {
3861
+ new Intl.DateTimeFormat(void 0, { timeZone });
3862
+ return true;
3863
+ } catch {
3864
+ return false;
3865
+ }
3866
+ }
3867
+ var ianaTz = z13.string().min(1).max(64).regex(/^[A-Za-z0-9_+\-/]+$/).refine(isKnownTimeZone, { message: "unknown IANA time zone" });
3868
+ var bookingAttendeeSchema = z13.object({
3869
+ email: z13.string().max(320).email(),
3870
+ display_name: z13.string().max(200).nullable().optional(),
3871
+ optional: z13.boolean().default(false)
3872
+ }).strict();
3873
+ var bookingReminderSchema = z13.object({
3874
+ method: z13.enum(["email", "popup"]),
3875
+ minutes_before: z13.number().int().min(0).max(MAX_REMINDER_MINUTES)
3876
+ }).strict();
3877
+ var bookingRemindersSchema = z13.object({
3878
+ use_calendar_defaults: z13.boolean().default(true),
3879
+ overrides: z13.array(bookingReminderSchema).max(MAX_REMINDER_OVERRIDES).default([])
3880
+ }).strict();
3881
+ var bookingLocationSchema = z13.union([
3882
+ z13.object({ kind: z13.literal("google_meet") }).strict(),
3883
+ z13.object({ kind: z13.literal("physical"), address: z13.string().max(1024) }).strict(),
3884
+ z13.object({ kind: z13.literal("phone"), phone: z13.string().max(64) }).strict(),
3885
+ z13.object({ kind: z13.literal("custom"), text: z13.string().max(1024) }).strict()
3886
+ ]);
3887
+ var bookingLocationNullableSchema = z13.union([...bookingLocationSchema.options, z13.null()]);
3888
+ var bookingCustomerSchema = z13.object({
3889
+ // Required: Google needs an address to send the invite that confirms the
3890
+ // booking, and that invite is the whole confirmation mechanism.
3891
+ email: z13.string().max(320).email(),
3892
+ name: z13.string().max(200).nullable().optional(),
3893
+ phone: z13.string().max(64).nullable().optional(),
3894
+ sms_consent: z13.boolean().default(false)
3895
+ }).strict();
3896
+ var jsonScalar = z13.union([z13.string(), z13.number(), z13.boolean(), z13.null()]);
3897
+ var bookingMetadataSchema = z13.object({
3898
+ schema_version: z13.number().int().default(CALENDAR_METADATA_SCHEMA_VERSION),
3899
+ source: z13.enum(BOOKING_SOURCES).nullable().optional(),
3900
+ service: z13.string().max(64).nullable().optional(),
3901
+ visitor_id: z13.string().max(64).nullable().optional(),
3902
+ session_id: z13.string().max(64).nullable().optional(),
3903
+ gclid: z13.string().max(256).nullable().optional(),
3904
+ fbclid: z13.string().max(256).nullable().optional(),
3905
+ utm_source: z13.string().max(128).nullable().optional(),
3906
+ utm_medium: z13.string().max(128).nullable().optional(),
3907
+ utm_campaign: z13.string().max(128).nullable().optional(),
3908
+ utm_term: z13.string().max(128).nullable().optional(),
3909
+ utm_content: z13.string().max(128).nullable().optional(),
3910
+ landing_page: z13.string().max(2048).nullable().optional(),
3911
+ referrer: z13.string().max(2048).nullable().optional(),
3912
+ notes: z13.string().max(2e3).nullable().optional(),
3913
+ custom: z13.record(jsonScalar).nullable().optional()
3914
+ }).strict();
3915
+ function refineEndXor(value, ctx) {
3916
+ const hasEnd = value.end_at !== void 0 && value.end_at !== null;
3917
+ const hasDuration = value.duration_minutes !== void 0 && value.duration_minutes !== null;
3918
+ if (hasEnd === hasDuration) {
3919
+ ctx.addIssue({
3920
+ code: z13.ZodIssueCode.custom,
3921
+ message: "provide exactly one of end_at or duration_minutes",
3922
+ path: ["end_at"]
3923
+ });
3924
+ return;
3925
+ }
3926
+ if (hasEnd && new Date(value.end_at) <= new Date(value.start_at)) {
3927
+ ctx.addIssue({
3928
+ code: z13.ZodIssueCode.custom,
3929
+ message: "end_at must be after start_at",
3930
+ path: ["end_at"]
3931
+ });
3932
+ }
3933
+ }
3934
+ var bookingCreateSchema = z13.object({
3935
+ calendar_key: z13.string().min(1).max(64),
3936
+ // Free-form: appointment types are the client site's own vocabulary, not
3937
+ // configuration the dashboard holds.
3938
+ appointment_type: z13.string().max(64).nullable().optional(),
3939
+ start_at: isoInstant,
3940
+ end_at: isoInstant.nullable().optional(),
3941
+ duration_minutes: z13.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),
3942
+ /** The booker's zone — distinct from the business's and the calendar's. */
3943
+ time_zone: ianaTz,
3944
+ title: z13.string().max(300).nullable().optional(),
3945
+ description: z13.string().max(8e3).nullable().optional(),
3946
+ location: bookingLocationNullableSchema.optional(),
3947
+ customer: bookingCustomerSchema,
3948
+ attendees: z13.array(bookingAttendeeSchema).max(MAX_ATTENDEES).default([]),
3949
+ send_invites: z13.boolean().default(true),
3950
+ reminders: bookingRemindersSchema.nullable().optional(),
3951
+ metadata: bookingMetadataSchema.nullable().optional(),
3952
+ idempotency_key: z13.string().max(128).nullable().optional(),
3953
+ buffer_before_minutes: z13.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0),
3954
+ buffer_after_minutes: z13.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0)
3955
+ }).strict().superRefine(refineEndXor);
3956
+ var bookingRescheduleSchema = z13.object({
3957
+ start_at: isoInstant,
3958
+ end_at: isoInstant.nullable().optional(),
3959
+ duration_minutes: z13.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),
3960
+ time_zone: ianaTz,
3961
+ notify_attendees: z13.boolean().default(true),
3962
+ reason: z13.string().max(500).nullable().optional()
3963
+ }).strict().superRefine(refineEndXor);
3964
+ var bookingCancelSchema = z13.object({
3965
+ reason: z13.string().max(500).nullable().optional(),
3966
+ notify_attendees: z13.boolean().default(true)
3967
+ }).strict();
3968
+
3969
+ // ../tracking-core/src/resources/calendar/slots.ts
3970
+ function parseHhMm(value) {
3971
+ const [hour, minute] = value.split(":").map(Number);
3972
+ return [hour || 0, minute || 0];
3973
+ }
3974
+ function dateKeyOf(date, timeZone) {
3975
+ const fmt = new Intl.DateTimeFormat("en-CA", {
3976
+ timeZone,
3977
+ year: "numeric",
3978
+ month: "2-digit",
3979
+ day: "2-digit"
3980
+ });
3981
+ return fmt.format(date);
3982
+ }
3983
+ function weekdayFromDateKey(dateKey) {
3984
+ const [year, month, day] = dateKey.split("-").map(Number);
3985
+ return new Date(Date.UTC(year, month - 1, day, 12)).getUTCDay();
3986
+ }
3987
+ function startOfLocalDay(at, timeZone) {
3988
+ const [year, month, day] = dateKeyOf(at, timeZone).split("-").map(Number);
3989
+ return instantFromWallClock(timeZone, year, month, day, 12, 0);
3990
+ }
3991
+ function overlaps(aStart, aEnd, bStart, bEnd) {
3992
+ return aStart < bEnd && aEnd > bStart;
3993
+ }
3994
+ function computeSlots(options) {
3995
+ const {
3996
+ busy,
3997
+ openingHours,
3998
+ timeZone,
3999
+ durationMinutes: durationMinutes2,
4000
+ granularityMinutes = 15,
4001
+ minNoticeMinutes = 0,
4002
+ bufferBeforeMinutes = 0,
4003
+ bufferAfterMinutes = 0,
4004
+ windowDays = 30,
4005
+ now = /* @__PURE__ */ new Date()
4006
+ } = options;
4007
+ const earliest = now.getTime() + minNoticeMinutes * 6e4;
4008
+ const busyRanges = busy.map((interval) => [
4009
+ new Date(interval.start_at).getTime(),
4010
+ new Date(interval.end_at).getTime()
4011
+ ]);
4012
+ const slots = [];
4013
+ let cursor = startOfLocalDay(now, timeZone);
4014
+ for (let dayOffset = 0; dayOffset < windowDays; dayOffset += 1) {
4015
+ const dateKey = dateKeyOf(cursor, timeZone);
4016
+ const [year, month, day] = dateKey.split("-").map(Number);
4017
+ cursor = instantFromWallClock(timeZone, year, month, day + 1, 12, 0);
4018
+ if (openingHours.closedDates?.includes(dateKey)) continue;
4019
+ const intervals = openingHours.overrides?.[dateKey] ?? openingHours.weekly[weekdayFromDateKey(dateKey)] ?? [];
4020
+ for (const interval of intervals) {
4021
+ const [openHour, openMinute] = parseHhMm(interval.start);
4022
+ const [closeHour, closeMinute] = parseHhMm(interval.end);
4023
+ const opensAt = instantFromWallClock(timeZone, year, month, day, openHour, openMinute);
4024
+ const closesAt = instantFromWallClock(timeZone, year, month, day, closeHour, closeMinute);
4025
+ for (let start = opensAt.getTime(); start + durationMinutes2 * 6e4 <= closesAt.getTime(); start += granularityMinutes * 6e4) {
4026
+ const end = start + durationMinutes2 * 6e4;
4027
+ if (start < earliest) continue;
4028
+ const blockedStart = start - bufferBeforeMinutes * 6e4;
4029
+ const blockedEnd = end + bufferAfterMinutes * 6e4;
4030
+ const taken = busyRanges.some(
4031
+ ([busyStart, busyEnd]) => overlaps(blockedStart, blockedEnd, busyStart, busyEnd)
4032
+ );
4033
+ if (taken) continue;
4034
+ slots.push({
4035
+ start_at: new Date(start).toISOString(),
4036
+ end_at: new Date(end).toISOString()
4037
+ });
4038
+ }
4039
+ }
4040
+ }
4041
+ slots.sort((a, b) => a.start_at.localeCompare(b.start_at));
4042
+ return { slots, nextAvailableAt: slots[0]?.start_at ?? null };
4043
+ }
4044
+ function groupSlotsByDay(slots, timeZone) {
4045
+ const byDay = /* @__PURE__ */ new Map();
4046
+ for (const slot of slots) {
4047
+ const key = dateKeyOf(new Date(slot.start_at), timeZone);
4048
+ const bucket = byDay.get(key);
4049
+ if (bucket) bucket.push(slot);
4050
+ else byDay.set(key, [slot]);
4051
+ }
4052
+ return [...byDay.entries()].map(([date, daySlots]) => ({ date, slots: daySlots })).sort((a, b) => a.date.localeCompare(b.date));
4053
+ }
4054
+
4055
+ // ../tracking-core/src/calendar-react.tsx
4056
+ import { createContext as createContext3, useCallback as useCallback4, useContext as useContext3, useEffect as useEffect5, useMemo as useMemo3, useState as useState4 } from "react";
4057
+ import { jsx as jsx6 } from "react/jsx-runtime";
4058
+ var _calendarClientContext;
4059
+ function calendarClientContext() {
4060
+ return _calendarClientContext ?? (_calendarClientContext = createContext3(null));
4061
+ }
4062
+ function CalendarClientProvider({
4063
+ value,
4064
+ children
4065
+ }) {
4066
+ const Ctx = calendarClientContext();
4067
+ return /* @__PURE__ */ jsx6(Ctx.Provider, { value, children });
4068
+ }
4069
+ function useCalendarClient() {
4070
+ const client = useContext3(calendarClientContext());
4071
+ if (!client) {
4072
+ throw new Error(
4073
+ "useCalendarClient requires <CalendarClientProvider>. Wire it via createTracking({ calendar })."
4074
+ );
4075
+ }
4076
+ return client;
4077
+ }
4078
+ function useCalendarBusy(calendarKey, window2) {
4079
+ const client = useCalendarClient();
4080
+ const [busy, setBusy] = useState4([]);
4081
+ const [timeZone, setTimeZone] = useState4(resolveBrowserTimeZone());
4082
+ const [isLoading, setIsLoading] = useState4(true);
4083
+ const [error, setError] = useState4(null);
4084
+ const [nonce, setNonce] = useState4(0);
4085
+ useEffect5(() => {
4086
+ const controller = new AbortController();
4087
+ setIsLoading(true);
4088
+ client.busy(calendarKey, { since: window2.since, until: window2.until }).then((response) => {
4089
+ if (controller.signal.aborted) return;
4090
+ setBusy(response.busy);
4091
+ setTimeZone(response.time_zone);
4092
+ setError(null);
4093
+ }).catch((cause) => {
4094
+ if (controller.signal.aborted) return;
4095
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
4096
+ }).finally(() => {
4097
+ if (!controller.signal.aborted) setIsLoading(false);
4098
+ });
4099
+ return () => controller.abort();
4100
+ }, [client, calendarKey, window2.since, window2.until, nonce]);
4101
+ const refetch = useCallback4(() => setNonce((value) => value + 1), []);
4102
+ return { busy, timeZone, isLoading, error, refetch };
4103
+ }
4104
+ function useSlots(busy, timeZone, options) {
4105
+ const zone = options.timeZone ?? timeZone;
4106
+ return useMemo3(() => {
4107
+ const { slots, nextAvailableAt } = computeSlots({ ...options, busy, timeZone: zone });
4108
+ return { slots, days: groupSlotsByDay(slots, zone), nextAvailableAt };
4109
+ }, [busy, zone, options]);
4110
+ }
4111
+ function useBookingForm(options = {}) {
4112
+ const endpoint = options.endpoint ?? "/api/aranova/calendar/bookings";
4113
+ const [status, setStatus] = useState4("idle");
4114
+ const [error, setError] = useState4(null);
4115
+ const [booking, setBooking] = useState4(null);
4116
+ const submit = useCallback4(
4117
+ async (input) => {
4118
+ setStatus("submitting");
4119
+ setError(null);
4120
+ try {
4121
+ const response = await fetch(endpoint, {
4122
+ method: "POST",
4123
+ headers: { "Content-Type": "application/json" },
4124
+ body: JSON.stringify(input)
4125
+ });
4126
+ const body = await response.json().catch(() => null);
4127
+ if (!response.ok) {
4128
+ const detail = body && typeof body === "object" && typeof body.detail === "string" ? body.detail : "Booking failed";
4129
+ throw new Error(detail);
4130
+ }
4131
+ setBooking(body);
4132
+ setStatus("confirmed");
4133
+ options.onConfirmed?.(body);
4134
+ } catch (cause) {
4135
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
4136
+ setStatus("failed");
4137
+ }
4138
+ },
4139
+ [endpoint, options]
4140
+ );
4141
+ const reset = useCallback4(() => {
4142
+ setStatus("idle");
4143
+ setError(null);
4144
+ setBooking(null);
4145
+ }, []);
4146
+ return { status, error, booking, submit, reset };
4147
+ }
3677
4148
  export {
3678
4149
  AdPlatformTracking,
3679
4150
  AranovaApiError,
4151
+ BOOKING_SOURCES,
4152
+ BOOKING_STATUSES,
4153
+ CALENDAR_ERROR_CODES,
4154
+ CALENDAR_METADATA_SCHEMA_VERSION,
4155
+ CALENDAR_SYNC_STATUSES,
4156
+ CalendarClientProvider,
3680
4157
  ConsentBanner,
3681
4158
  DEFAULT_DECLINE_TTL_DAYS,
3682
4159
  DEFAULT_PHONE_COUNTRY,
3683
4160
  EVENT_SEMANTICS,
4161
+ GOOGLE_SYNC_STATES,
3684
4162
  GoogleAdsTracking,
4163
+ MAX_ATTENDEES,
4164
+ MAX_BOOKING_MINUTES,
4165
+ MAX_REMINDER_MINUTES,
4166
+ MAX_REMINDER_OVERRIDES,
3685
4167
  NAMED_RANGES,
3686
4168
  PhoneField,
3687
4169
  SUPPORTED_CURRENCIES,
3688
4170
  TRACKING_PARAM_KEYS,
3689
4171
  TRACKING_RANGES,
4172
+ bookingAttendeeSchema,
4173
+ bookingCancelSchema,
4174
+ bookingCreateSchema,
4175
+ bookingCustomerSchema,
4176
+ bookingLocationNullableSchema,
4177
+ bookingLocationSchema,
4178
+ bookingMetadataSchema,
4179
+ bookingReminderSchema,
4180
+ bookingRemindersSchema,
4181
+ bookingRescheduleSchema,
3690
4182
  captureTrackingParamsFromLocation,
3691
4183
  clearStashedUserData,
4184
+ computeSlots,
4185
+ createCalendarReadClient,
3692
4186
  createSalesClient,
3693
4187
  createTracking,
3694
4188
  createTrackingClientContext,
3695
4189
  createTrackingEventCreatePayload,
3696
4190
  createTrackingSessionUpsertPayload,
4191
+ durationMinutes,
4192
+ endOfDayInTz,
3697
4193
  fetchServices,
3698
4194
  formatDateInTz,
3699
4195
  formatMoney,
3700
4196
  formatPhone,
3701
4197
  formatPhoneAsTyped,
4198
+ formatSlotRange,
3702
4199
  fromMinor,
3703
4200
  getConsentChoice,
3704
4201
  getConsentState,
3705
4202
  getEventSemantics,
3706
4203
  getTrackingConfigRuntime,
4204
+ groupSlotsByDay,
4205
+ instantFromWallClock,
4206
+ isCalendarError,
4207
+ localDateKey,
4208
+ offsetMinutesAt,
3707
4209
  onConsentChange,
3708
4210
  optIn,
3709
4211
  optOut,
3710
4212
  parsePhone,
3711
4213
  phoneField,
3712
4214
  resetConsent,
4215
+ resolveBrowserTimeZone,
3713
4216
  resolveConversionConfig,
3714
4217
  saleCreateSchema,
3715
4218
  saleItemSchema,
@@ -3717,15 +4220,20 @@ export {
3717
4220
  saleUpdateSchema,
3718
4221
  salesRequest,
3719
4222
  setConsentState,
4223
+ startOfDayInTz,
3720
4224
  stashUserData,
3721
4225
  toE164,
3722
4226
  toMinor,
4227
+ useBookingForm,
4228
+ useCalendarBusy,
4229
+ useCalendarClient,
3723
4230
  useConsent,
3724
4231
  useConsentState,
3725
4232
  useCookiePreferences,
3726
4233
  useGclid,
3727
4234
  usePhoneConfig,
3728
4235
  usePhoneField,
4236
+ useSlots,
3729
4237
  useTrackingParams
3730
4238
  };
3731
4239
  //# sourceMappingURL=index.mjs.map