@aranova/tracking-next 0.20.2 → 0.21.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,39 +32,71 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  AdPlatformTracking: () => AdPlatformTracking,
34
34
  AranovaApiError: () => AranovaApiError,
35
+ BOOKING_SOURCES: () => BOOKING_SOURCES,
36
+ BOOKING_STATUSES: () => BOOKING_STATUSES,
37
+ CALENDAR_ERROR_CODES: () => CALENDAR_ERROR_CODES,
38
+ CALENDAR_METADATA_SCHEMA_VERSION: () => CALENDAR_METADATA_SCHEMA_VERSION,
39
+ CALENDAR_SYNC_STATUSES: () => CALENDAR_SYNC_STATUSES,
40
+ CalendarClientProvider: () => CalendarClientProvider,
35
41
  ConsentBanner: () => ConsentBanner,
36
42
  DEFAULT_DECLINE_TTL_DAYS: () => DEFAULT_DECLINE_TTL_DAYS,
37
43
  DEFAULT_PHONE_COUNTRY: () => DEFAULT_PHONE_COUNTRY,
38
44
  EVENT_SEMANTICS: () => EVENT_SEMANTICS,
45
+ GOOGLE_SYNC_STATES: () => GOOGLE_SYNC_STATES,
39
46
  GoogleAdsTracking: () => GoogleAdsTracking,
47
+ MAX_ATTENDEES: () => MAX_ATTENDEES,
48
+ MAX_BOOKING_MINUTES: () => MAX_BOOKING_MINUTES,
49
+ MAX_REMINDER_MINUTES: () => MAX_REMINDER_MINUTES,
50
+ MAX_REMINDER_OVERRIDES: () => MAX_REMINDER_OVERRIDES,
40
51
  NAMED_RANGES: () => NAMED_RANGES,
41
52
  PhoneField: () => PhoneField,
42
53
  SUPPORTED_CURRENCIES: () => SUPPORTED_CURRENCIES,
43
54
  TRACKING_PARAM_KEYS: () => TRACKING_PARAM_KEYS,
44
55
  TRACKING_RANGES: () => TRACKING_RANGES,
56
+ bookingAttendeeSchema: () => bookingAttendeeSchema,
57
+ bookingCancelSchema: () => bookingCancelSchema,
58
+ bookingCreateSchema: () => bookingCreateSchema,
59
+ bookingCustomerSchema: () => bookingCustomerSchema,
60
+ bookingLocationNullableSchema: () => bookingLocationNullableSchema,
61
+ bookingLocationSchema: () => bookingLocationSchema,
62
+ bookingMetadataSchema: () => bookingMetadataSchema,
63
+ bookingReminderSchema: () => bookingReminderSchema,
64
+ bookingRemindersSchema: () => bookingRemindersSchema,
65
+ bookingRescheduleSchema: () => bookingRescheduleSchema,
45
66
  captureTrackingParamsFromLocation: () => captureTrackingParamsFromLocation,
46
67
  clearStashedUserData: () => clearStashedUserData,
68
+ computeSlots: () => computeSlots,
69
+ createCalendarReadClient: () => createCalendarReadClient,
47
70
  createSalesClient: () => createSalesClient,
48
71
  createTracking: () => createTracking,
49
72
  createTrackingClientContext: () => createTrackingClientContext,
50
73
  createTrackingEventCreatePayload: () => createTrackingEventCreatePayload,
51
74
  createTrackingSessionUpsertPayload: () => createTrackingSessionUpsertPayload,
75
+ durationMinutes: () => durationMinutes,
76
+ endOfDayInTz: () => endOfDayInTz,
52
77
  fetchServices: () => fetchServices,
53
78
  formatDateInTz: () => formatDateInTz,
54
79
  formatMoney: () => formatMoney,
55
80
  formatPhone: () => formatPhone,
56
81
  formatPhoneAsTyped: () => formatPhoneAsTyped,
82
+ formatSlotRange: () => formatSlotRange,
57
83
  fromMinor: () => fromMinor,
58
84
  getConsentChoice: () => getConsentChoice,
59
85
  getConsentState: () => getConsentState,
60
86
  getEventSemantics: () => getEventSemantics,
61
87
  getTrackingConfigRuntime: () => getTrackingConfigRuntime,
88
+ groupSlotsByDay: () => groupSlotsByDay,
89
+ instantFromWallClock: () => instantFromWallClock,
90
+ isCalendarError: () => isCalendarError,
91
+ localDateKey: () => localDateKey,
92
+ offsetMinutesAt: () => offsetMinutesAt,
62
93
  onConsentChange: () => onConsentChange,
63
94
  optIn: () => optIn,
64
95
  optOut: () => optOut,
65
96
  parsePhone: () => parsePhone,
66
97
  phoneField: () => phoneField,
67
98
  resetConsent: () => resetConsent,
99
+ resolveBrowserTimeZone: () => resolveBrowserTimeZone,
68
100
  resolveConversionConfig: () => resolveConversionConfig,
69
101
  saleCreateSchema: () => saleCreateSchema,
70
102
  saleItemSchema: () => saleItemSchema,
@@ -72,15 +104,20 @@ __export(src_exports, {
72
104
  saleUpdateSchema: () => saleUpdateSchema,
73
105
  salesRequest: () => salesRequest,
74
106
  setConsentState: () => setConsentState,
107
+ startOfDayInTz: () => startOfDayInTz,
75
108
  stashUserData: () => stashUserData,
76
109
  toE164: () => toE164,
77
110
  toMinor: () => toMinor,
111
+ useBookingForm: () => useBookingForm,
112
+ useCalendarBusy: () => useCalendarBusy,
113
+ useCalendarClient: () => useCalendarClient,
78
114
  useConsent: () => useConsent,
79
115
  useConsentState: () => useConsentState,
80
116
  useCookiePreferences: () => useCookiePreferences,
81
117
  useGclid: () => useGclid,
82
118
  usePhoneConfig: () => usePhoneConfig,
83
119
  usePhoneField: () => usePhoneField,
120
+ useSlots: () => useSlots,
84
121
  useTrackingParams: () => useTrackingParams
85
122
  });
86
123
  module.exports = __toCommonJS(src_exports);
@@ -1033,6 +1070,21 @@ function automaticThresholdMet(goal, eventType, metadata) {
1033
1070
  }
1034
1071
  }
1035
1072
 
1073
+ // ../tracking-core/src/resources/intl/date.ts
1074
+ function formatDateInTz(iso, timeZone, opts, locale) {
1075
+ const date = new Date(iso);
1076
+ if (Number.isNaN(date.getTime())) return iso;
1077
+ return new Intl.DateTimeFormat(locale, {
1078
+ year: "numeric",
1079
+ month: "short",
1080
+ day: "2-digit",
1081
+ hour: "2-digit",
1082
+ minute: "2-digit",
1083
+ ...opts,
1084
+ timeZone
1085
+ }).format(date);
1086
+ }
1087
+
1036
1088
  // ../tracking-core/src/resources/sales/money.ts
1037
1089
  var MINOR_UNIT_EXPONENT = {
1038
1090
  USD: 2,
@@ -1052,19 +1104,6 @@ function formatMoney(cents, currency, locale) {
1052
1104
  fromMinor(cents, currency)
1053
1105
  );
1054
1106
  }
1055
- function formatDateInTz(iso, timeZone, opts, locale) {
1056
- const date = new Date(iso);
1057
- if (Number.isNaN(date.getTime())) return iso;
1058
- return new Intl.DateTimeFormat(locale, {
1059
- year: "numeric",
1060
- month: "short",
1061
- day: "2-digit",
1062
- hour: "2-digit",
1063
- minute: "2-digit",
1064
- ...opts,
1065
- timeZone
1066
- }).format(date);
1067
- }
1068
1107
 
1069
1108
  // ../tracking-core/src/resources/tracking-config-runtime.ts
1070
1109
  var CACHE_PREFIX2 = "_aranova_cfg_runtime_";
@@ -2767,7 +2806,7 @@ function attachPhoneClickCapture(client, config) {
2767
2806
  };
2768
2807
  }
2769
2808
 
2770
- // ../tracking-core/src/resources/sales/errors.ts
2809
+ // ../tracking-core/src/resources/http/errors.ts
2771
2810
  var AranovaApiError = class extends Error {
2772
2811
  constructor(message, options) {
2773
2812
  super(message);
@@ -2778,7 +2817,7 @@ var AranovaApiError = class extends Error {
2778
2817
  }
2779
2818
  };
2780
2819
 
2781
- // ../tracking-core/src/resources/sales/transport.ts
2820
+ // ../tracking-core/src/resources/http/request.ts
2782
2821
  function identityHeaders(config) {
2783
2822
  const headers = { [API_KEY_HEADER]: config.apiKey };
2784
2823
  if (config.sdkVersion) headers[SDK_VERSION_HEADER] = config.sdkVersion;
@@ -2790,8 +2829,8 @@ function identityHeaders(config) {
2790
2829
  function joinUrl(endpoint, path) {
2791
2830
  return `${endpoint.replace(/\/$/, "")}${path}`;
2792
2831
  }
2793
- async function salesRequest(config, method, path, body) {
2794
- const headers = identityHeaders(config);
2832
+ async function apiRequest(config, method, path, body, extraHeaders) {
2833
+ const headers = { ...identityHeaders(config), ...extraHeaders };
2795
2834
  if (body !== void 0) headers["Content-Type"] = "application/json";
2796
2835
  const response = await fetch(joinUrl(config.endpoint, path), {
2797
2836
  method,
@@ -2820,6 +2859,9 @@ async function salesRequest(config, method, path, body) {
2820
2859
  return await response.json();
2821
2860
  }
2822
2861
 
2862
+ // ../tracking-core/src/resources/sales/transport.ts
2863
+ var salesRequest = apiRequest;
2864
+
2823
2865
  // ../tracking-core/src/resources/sales/client.ts
2824
2866
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
2825
2867
  if (!firing) return;
@@ -3465,7 +3507,7 @@ function GoogleAdsTracking(props) {
3465
3507
  var import_navigation3 = require("next/navigation");
3466
3508
 
3467
3509
  // package.json
3468
- var version = "0.20.2";
3510
+ var version = "0.21.0";
3469
3511
 
3470
3512
  // src/factory.tsx
3471
3513
  var import_react5 = require("react");
@@ -3746,43 +3788,489 @@ function createTracking(options) {
3746
3788
  }
3747
3789
  return { TrackingProvider, useTracking };
3748
3790
  }
3791
+
3792
+ // ../tracking-core/src/resources/calendar/transport.ts
3793
+ async function calendarRequest(config, method, path, body) {
3794
+ return apiRequest(config, method, `/calendar${path}`, body);
3795
+ }
3796
+
3797
+ // ../tracking-core/src/resources/calendar/time.ts
3798
+ function resolveBrowserTimeZone() {
3799
+ try {
3800
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
3801
+ } catch {
3802
+ return "UTC";
3803
+ }
3804
+ }
3805
+ function partsInTz(date, timeZone) {
3806
+ const formatter = new Intl.DateTimeFormat("en-US", {
3807
+ timeZone,
3808
+ hour12: false,
3809
+ year: "numeric",
3810
+ month: "2-digit",
3811
+ day: "2-digit",
3812
+ hour: "2-digit",
3813
+ minute: "2-digit",
3814
+ second: "2-digit"
3815
+ });
3816
+ const out = {};
3817
+ for (const part of formatter.formatToParts(date)) {
3818
+ if (part.type !== "literal") out[part.type] = Number(part.value);
3819
+ }
3820
+ if (out.hour === 24) out.hour = 0;
3821
+ return out;
3822
+ }
3823
+ function offsetMinutesAt(date, timeZone) {
3824
+ const p = partsInTz(date, timeZone);
3825
+ const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
3826
+ return (asUtc - Math.floor(date.getTime() / 1e3) * 1e3) / 6e4;
3827
+ }
3828
+ function instantFromWallClock(timeZone, year, month, day, hour = 0, minute = 0) {
3829
+ const naive = Date.UTC(year, month - 1, day, hour, minute);
3830
+ const firstGuess = new Date(naive - offsetMinutesAt(new Date(naive), timeZone) * 6e4);
3831
+ const corrected = naive - offsetMinutesAt(firstGuess, timeZone) * 6e4;
3832
+ return new Date(corrected);
3833
+ }
3834
+ function localDateKey(iso, timeZone) {
3835
+ const p = partsInTz(new Date(iso), timeZone);
3836
+ const pad = (n) => String(n).padStart(2, "0");
3837
+ return `${p.year}-${pad(p.month)}-${pad(p.day)}`;
3838
+ }
3839
+ function startOfDayInTz(dateKey, timeZone) {
3840
+ const [year, month, day] = dateKey.split("-").map(Number);
3841
+ return instantFromWallClock(timeZone, year, month, day, 0, 0);
3842
+ }
3843
+ function endOfDayInTz(dateKey, timeZone) {
3844
+ const start = startOfDayInTz(dateKey, timeZone);
3845
+ const p = partsInTz(start, timeZone);
3846
+ return instantFromWallClock(timeZone, p.year, p.month, p.day + 1, 0, 0);
3847
+ }
3848
+ function durationMinutes(startIso, endIso) {
3849
+ return Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 6e4);
3850
+ }
3851
+ function formatSlotRange(startIso, endIso, timeZone, locale) {
3852
+ const opts = { hour: "2-digit", minute: "2-digit", timeZone };
3853
+ const fmt = new Intl.DateTimeFormat(locale, opts);
3854
+ return `${fmt.format(new Date(startIso))} \u2013 ${fmt.format(new Date(endIso))}`;
3855
+ }
3856
+
3857
+ // ../tracking-core/src/resources/calendar/read-client.ts
3858
+ function withBusiness(config, path) {
3859
+ if (!config.businessId) return path;
3860
+ const separator = path.includes("?") ? "&" : "?";
3861
+ return `${path}${separator}business_id=${encodeURIComponent(config.businessId)}`;
3862
+ }
3863
+ function createCalendarReadClient(config) {
3864
+ return {
3865
+ async listCalendars() {
3866
+ return calendarRequest(config, "GET", withBusiness(config, "/calendars"));
3867
+ },
3868
+ async busy(calendarKey, query) {
3869
+ const params = new URLSearchParams({
3870
+ since: query.since,
3871
+ until: query.until
3872
+ });
3873
+ const timeZone = query.timeZone ?? config.defaultTimeZone ?? resolveBrowserTimeZone();
3874
+ params.set("time_zone", timeZone);
3875
+ return calendarRequest(
3876
+ config,
3877
+ "GET",
3878
+ withBusiness(config, `/${encodeURIComponent(calendarKey)}/busy?${params.toString()}`)
3879
+ );
3880
+ }
3881
+ };
3882
+ }
3883
+ function isCalendarError(error, code) {
3884
+ if (!(error instanceof AranovaApiError)) return false;
3885
+ return code === void 0 || error.code === code;
3886
+ }
3887
+
3888
+ // ../tracking-core/src/resources/calendar/schema.ts
3889
+ var import_zod13 = require("zod");
3890
+ var CALENDAR_METADATA_SCHEMA_VERSION = 1;
3891
+ var MAX_ATTENDEES = 20;
3892
+ var MAX_REMINDER_OVERRIDES = 5;
3893
+ var MAX_REMINDER_MINUTES = 40320;
3894
+ var MAX_BOOKING_MINUTES = 1440;
3895
+ var BOOKING_SOURCES = ["website", "phone", "walk_in", "import", "operator"];
3896
+ var BOOKING_STATUSES = ["pending", "confirmed", "cancelled", "no_show"];
3897
+ var GOOGLE_SYNC_STATES = ["queued", "synced", "uncertain", "failed", "drifted"];
3898
+ var CALENDAR_SYNC_STATUSES = [
3899
+ "pending_invite",
3900
+ "awaiting_calendar_selection",
3901
+ "connected",
3902
+ "needs_reconnect",
3903
+ "revoked",
3904
+ "disabled"
3905
+ ];
3906
+ var CALENDAR_ERROR_CODES = [
3907
+ "slot_unavailable",
3908
+ "calendar_not_connected",
3909
+ "calendar_disconnected",
3910
+ "booking_not_found",
3911
+ "outside_booking_window",
3912
+ "rate_limited",
3913
+ "google_error"
3914
+ ];
3915
+ var isoInstant = import_zod13.z.string().datetime({ offset: true });
3916
+ function isKnownTimeZone(timeZone) {
3917
+ try {
3918
+ new Intl.DateTimeFormat(void 0, { timeZone });
3919
+ return true;
3920
+ } catch {
3921
+ return false;
3922
+ }
3923
+ }
3924
+ var ianaTz = import_zod13.z.string().min(1).max(64).regex(/^[A-Za-z0-9_+\-/]+$/).refine(isKnownTimeZone, { message: "unknown IANA time zone" });
3925
+ var bookingAttendeeSchema = import_zod13.z.object({
3926
+ email: import_zod13.z.string().max(320).email(),
3927
+ display_name: import_zod13.z.string().max(200).nullable().optional(),
3928
+ optional: import_zod13.z.boolean().default(false)
3929
+ }).strict();
3930
+ var bookingReminderSchema = import_zod13.z.object({
3931
+ method: import_zod13.z.enum(["email", "popup"]),
3932
+ minutes_before: import_zod13.z.number().int().min(0).max(MAX_REMINDER_MINUTES)
3933
+ }).strict();
3934
+ var bookingRemindersSchema = import_zod13.z.object({
3935
+ use_calendar_defaults: import_zod13.z.boolean().default(true),
3936
+ overrides: import_zod13.z.array(bookingReminderSchema).max(MAX_REMINDER_OVERRIDES).default([])
3937
+ }).strict();
3938
+ var bookingLocationSchema = import_zod13.z.union([
3939
+ import_zod13.z.object({ kind: import_zod13.z.literal("google_meet") }).strict(),
3940
+ import_zod13.z.object({ kind: import_zod13.z.literal("physical"), address: import_zod13.z.string().max(1024) }).strict(),
3941
+ import_zod13.z.object({ kind: import_zod13.z.literal("phone"), phone: import_zod13.z.string().max(64) }).strict(),
3942
+ import_zod13.z.object({ kind: import_zod13.z.literal("custom"), text: import_zod13.z.string().max(1024) }).strict()
3943
+ ]);
3944
+ var bookingLocationNullableSchema = import_zod13.z.union([...bookingLocationSchema.options, import_zod13.z.null()]);
3945
+ var bookingCustomerSchema = import_zod13.z.object({
3946
+ // Required: Google needs an address to send the invite that confirms the
3947
+ // booking, and that invite is the whole confirmation mechanism.
3948
+ email: import_zod13.z.string().max(320).email(),
3949
+ name: import_zod13.z.string().max(200).nullable().optional(),
3950
+ phone: import_zod13.z.string().max(64).nullable().optional(),
3951
+ sms_consent: import_zod13.z.boolean().default(false)
3952
+ }).strict();
3953
+ var jsonScalar = import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean(), import_zod13.z.null()]);
3954
+ var bookingMetadataSchema = import_zod13.z.object({
3955
+ schema_version: import_zod13.z.number().int().default(CALENDAR_METADATA_SCHEMA_VERSION),
3956
+ source: import_zod13.z.enum(BOOKING_SOURCES).nullable().optional(),
3957
+ service: import_zod13.z.string().max(64).nullable().optional(),
3958
+ visitor_id: import_zod13.z.string().max(64).nullable().optional(),
3959
+ session_id: import_zod13.z.string().max(64).nullable().optional(),
3960
+ gclid: import_zod13.z.string().max(256).nullable().optional(),
3961
+ fbclid: import_zod13.z.string().max(256).nullable().optional(),
3962
+ utm_source: import_zod13.z.string().max(128).nullable().optional(),
3963
+ utm_medium: import_zod13.z.string().max(128).nullable().optional(),
3964
+ utm_campaign: import_zod13.z.string().max(128).nullable().optional(),
3965
+ utm_term: import_zod13.z.string().max(128).nullable().optional(),
3966
+ utm_content: import_zod13.z.string().max(128).nullable().optional(),
3967
+ landing_page: import_zod13.z.string().max(2048).nullable().optional(),
3968
+ referrer: import_zod13.z.string().max(2048).nullable().optional(),
3969
+ notes: import_zod13.z.string().max(2e3).nullable().optional(),
3970
+ custom: import_zod13.z.record(jsonScalar).nullable().optional()
3971
+ }).strict();
3972
+ function refineEndXor(value, ctx) {
3973
+ const hasEnd = value.end_at !== void 0 && value.end_at !== null;
3974
+ const hasDuration = value.duration_minutes !== void 0 && value.duration_minutes !== null;
3975
+ if (hasEnd === hasDuration) {
3976
+ ctx.addIssue({
3977
+ code: import_zod13.z.ZodIssueCode.custom,
3978
+ message: "provide exactly one of end_at or duration_minutes",
3979
+ path: ["end_at"]
3980
+ });
3981
+ return;
3982
+ }
3983
+ if (hasEnd && new Date(value.end_at) <= new Date(value.start_at)) {
3984
+ ctx.addIssue({
3985
+ code: import_zod13.z.ZodIssueCode.custom,
3986
+ message: "end_at must be after start_at",
3987
+ path: ["end_at"]
3988
+ });
3989
+ }
3990
+ }
3991
+ var bookingCreateSchema = import_zod13.z.object({
3992
+ calendar_key: import_zod13.z.string().min(1).max(64),
3993
+ // Free-form: appointment types are the client site's own vocabulary, not
3994
+ // configuration the dashboard holds.
3995
+ appointment_type: import_zod13.z.string().max(64).nullable().optional(),
3996
+ start_at: isoInstant,
3997
+ end_at: isoInstant.nullable().optional(),
3998
+ duration_minutes: import_zod13.z.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),
3999
+ /** The booker's zone — distinct from the business's and the calendar's. */
4000
+ time_zone: ianaTz,
4001
+ title: import_zod13.z.string().max(300).nullable().optional(),
4002
+ description: import_zod13.z.string().max(8e3).nullable().optional(),
4003
+ location: bookingLocationNullableSchema.optional(),
4004
+ customer: bookingCustomerSchema,
4005
+ attendees: import_zod13.z.array(bookingAttendeeSchema).max(MAX_ATTENDEES).default([]),
4006
+ send_invites: import_zod13.z.boolean().default(true),
4007
+ reminders: bookingRemindersSchema.nullable().optional(),
4008
+ metadata: bookingMetadataSchema.nullable().optional(),
4009
+ idempotency_key: import_zod13.z.string().max(128).nullable().optional(),
4010
+ buffer_before_minutes: import_zod13.z.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0),
4011
+ buffer_after_minutes: import_zod13.z.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0)
4012
+ }).strict().superRefine(refineEndXor);
4013
+ var bookingRescheduleSchema = import_zod13.z.object({
4014
+ start_at: isoInstant,
4015
+ end_at: isoInstant.nullable().optional(),
4016
+ duration_minutes: import_zod13.z.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),
4017
+ time_zone: ianaTz,
4018
+ notify_attendees: import_zod13.z.boolean().default(true),
4019
+ reason: import_zod13.z.string().max(500).nullable().optional()
4020
+ }).strict().superRefine(refineEndXor);
4021
+ var bookingCancelSchema = import_zod13.z.object({
4022
+ reason: import_zod13.z.string().max(500).nullable().optional(),
4023
+ notify_attendees: import_zod13.z.boolean().default(true)
4024
+ }).strict();
4025
+
4026
+ // ../tracking-core/src/resources/calendar/slots.ts
4027
+ function parseHhMm(value) {
4028
+ const [hour, minute] = value.split(":").map(Number);
4029
+ return [hour || 0, minute || 0];
4030
+ }
4031
+ function dateKeyOf(date, timeZone) {
4032
+ const fmt = new Intl.DateTimeFormat("en-CA", {
4033
+ timeZone,
4034
+ year: "numeric",
4035
+ month: "2-digit",
4036
+ day: "2-digit"
4037
+ });
4038
+ return fmt.format(date);
4039
+ }
4040
+ function weekdayFromDateKey(dateKey) {
4041
+ const [year, month, day] = dateKey.split("-").map(Number);
4042
+ return new Date(Date.UTC(year, month - 1, day, 12)).getUTCDay();
4043
+ }
4044
+ function startOfLocalDay(at, timeZone) {
4045
+ const [year, month, day] = dateKeyOf(at, timeZone).split("-").map(Number);
4046
+ return instantFromWallClock(timeZone, year, month, day, 12, 0);
4047
+ }
4048
+ function overlaps(aStart, aEnd, bStart, bEnd) {
4049
+ return aStart < bEnd && aEnd > bStart;
4050
+ }
4051
+ function computeSlots(options) {
4052
+ const {
4053
+ busy,
4054
+ openingHours,
4055
+ timeZone,
4056
+ durationMinutes: durationMinutes2,
4057
+ granularityMinutes = 15,
4058
+ minNoticeMinutes = 0,
4059
+ bufferBeforeMinutes = 0,
4060
+ bufferAfterMinutes = 0,
4061
+ windowDays = 30,
4062
+ now = /* @__PURE__ */ new Date()
4063
+ } = options;
4064
+ const earliest = now.getTime() + minNoticeMinutes * 6e4;
4065
+ const busyRanges = busy.map((interval) => [
4066
+ new Date(interval.start_at).getTime(),
4067
+ new Date(interval.end_at).getTime()
4068
+ ]);
4069
+ const slots = [];
4070
+ let cursor = startOfLocalDay(now, timeZone);
4071
+ for (let dayOffset = 0; dayOffset < windowDays; dayOffset += 1) {
4072
+ const dateKey = dateKeyOf(cursor, timeZone);
4073
+ const [year, month, day] = dateKey.split("-").map(Number);
4074
+ cursor = instantFromWallClock(timeZone, year, month, day + 1, 12, 0);
4075
+ if (openingHours.closedDates?.includes(dateKey)) continue;
4076
+ const intervals = openingHours.overrides?.[dateKey] ?? openingHours.weekly[weekdayFromDateKey(dateKey)] ?? [];
4077
+ for (const interval of intervals) {
4078
+ const [openHour, openMinute] = parseHhMm(interval.start);
4079
+ const [closeHour, closeMinute] = parseHhMm(interval.end);
4080
+ const opensAt = instantFromWallClock(timeZone, year, month, day, openHour, openMinute);
4081
+ const closesAt = instantFromWallClock(timeZone, year, month, day, closeHour, closeMinute);
4082
+ for (let start = opensAt.getTime(); start + durationMinutes2 * 6e4 <= closesAt.getTime(); start += granularityMinutes * 6e4) {
4083
+ const end = start + durationMinutes2 * 6e4;
4084
+ if (start < earliest) continue;
4085
+ const blockedStart = start - bufferBeforeMinutes * 6e4;
4086
+ const blockedEnd = end + bufferAfterMinutes * 6e4;
4087
+ const taken = busyRanges.some(
4088
+ ([busyStart, busyEnd]) => overlaps(blockedStart, blockedEnd, busyStart, busyEnd)
4089
+ );
4090
+ if (taken) continue;
4091
+ slots.push({
4092
+ start_at: new Date(start).toISOString(),
4093
+ end_at: new Date(end).toISOString()
4094
+ });
4095
+ }
4096
+ }
4097
+ }
4098
+ slots.sort((a, b) => a.start_at.localeCompare(b.start_at));
4099
+ return { slots, nextAvailableAt: slots[0]?.start_at ?? null };
4100
+ }
4101
+ function groupSlotsByDay(slots, timeZone) {
4102
+ const byDay = /* @__PURE__ */ new Map();
4103
+ for (const slot of slots) {
4104
+ const key = dateKeyOf(new Date(slot.start_at), timeZone);
4105
+ const bucket = byDay.get(key);
4106
+ if (bucket) bucket.push(slot);
4107
+ else byDay.set(key, [slot]);
4108
+ }
4109
+ return [...byDay.entries()].map(([date, daySlots]) => ({ date, slots: daySlots })).sort((a, b) => a.date.localeCompare(b.date));
4110
+ }
4111
+
4112
+ // ../tracking-core/src/calendar-react.tsx
4113
+ var import_react6 = require("react");
4114
+ var import_jsx_runtime6 = require("react/jsx-runtime");
4115
+ var _calendarClientContext;
4116
+ function calendarClientContext() {
4117
+ return _calendarClientContext ?? (_calendarClientContext = (0, import_react6.createContext)(null));
4118
+ }
4119
+ function CalendarClientProvider({
4120
+ value,
4121
+ children
4122
+ }) {
4123
+ const Ctx = calendarClientContext();
4124
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Ctx.Provider, { value, children });
4125
+ }
4126
+ function useCalendarClient() {
4127
+ const client = (0, import_react6.useContext)(calendarClientContext());
4128
+ if (!client) {
4129
+ throw new Error(
4130
+ "useCalendarClient requires <CalendarClientProvider>. Wire it via createTracking({ calendar })."
4131
+ );
4132
+ }
4133
+ return client;
4134
+ }
4135
+ function useCalendarBusy(calendarKey, window2) {
4136
+ const client = useCalendarClient();
4137
+ const [busy, setBusy] = (0, import_react6.useState)([]);
4138
+ const [timeZone, setTimeZone] = (0, import_react6.useState)(resolveBrowserTimeZone());
4139
+ const [isLoading, setIsLoading] = (0, import_react6.useState)(true);
4140
+ const [error, setError] = (0, import_react6.useState)(null);
4141
+ const [nonce, setNonce] = (0, import_react6.useState)(0);
4142
+ (0, import_react6.useEffect)(() => {
4143
+ const controller = new AbortController();
4144
+ setIsLoading(true);
4145
+ client.busy(calendarKey, { since: window2.since, until: window2.until }).then((response) => {
4146
+ if (controller.signal.aborted) return;
4147
+ setBusy(response.busy);
4148
+ setTimeZone(response.time_zone);
4149
+ setError(null);
4150
+ }).catch((cause) => {
4151
+ if (controller.signal.aborted) return;
4152
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
4153
+ }).finally(() => {
4154
+ if (!controller.signal.aborted) setIsLoading(false);
4155
+ });
4156
+ return () => controller.abort();
4157
+ }, [client, calendarKey, window2.since, window2.until, nonce]);
4158
+ const refetch = (0, import_react6.useCallback)(() => setNonce((value) => value + 1), []);
4159
+ return { busy, timeZone, isLoading, error, refetch };
4160
+ }
4161
+ function useSlots(busy, timeZone, options) {
4162
+ const zone = options.timeZone ?? timeZone;
4163
+ return (0, import_react6.useMemo)(() => {
4164
+ const { slots, nextAvailableAt } = computeSlots({ ...options, busy, timeZone: zone });
4165
+ return { slots, days: groupSlotsByDay(slots, zone), nextAvailableAt };
4166
+ }, [busy, zone, options]);
4167
+ }
4168
+ function useBookingForm(options = {}) {
4169
+ const endpoint = options.endpoint ?? "/api/aranova/calendar/bookings";
4170
+ const [status, setStatus] = (0, import_react6.useState)("idle");
4171
+ const [error, setError] = (0, import_react6.useState)(null);
4172
+ const [booking, setBooking] = (0, import_react6.useState)(null);
4173
+ const submit = (0, import_react6.useCallback)(
4174
+ async (input) => {
4175
+ setStatus("submitting");
4176
+ setError(null);
4177
+ try {
4178
+ const response = await fetch(endpoint, {
4179
+ method: "POST",
4180
+ headers: { "Content-Type": "application/json" },
4181
+ body: JSON.stringify(input)
4182
+ });
4183
+ const body = await response.json().catch(() => null);
4184
+ if (!response.ok) {
4185
+ const detail = body && typeof body === "object" && typeof body.detail === "string" ? body.detail : "Booking failed";
4186
+ throw new Error(detail);
4187
+ }
4188
+ setBooking(body);
4189
+ setStatus("confirmed");
4190
+ options.onConfirmed?.(body);
4191
+ } catch (cause) {
4192
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
4193
+ setStatus("failed");
4194
+ }
4195
+ },
4196
+ [endpoint, options]
4197
+ );
4198
+ const reset = (0, import_react6.useCallback)(() => {
4199
+ setStatus("idle");
4200
+ setError(null);
4201
+ setBooking(null);
4202
+ }, []);
4203
+ return { status, error, booking, submit, reset };
4204
+ }
3749
4205
  // Annotate the CommonJS export names for ESM import in node:
3750
4206
  0 && (module.exports = {
3751
4207
  AdPlatformTracking,
3752
4208
  AranovaApiError,
4209
+ BOOKING_SOURCES,
4210
+ BOOKING_STATUSES,
4211
+ CALENDAR_ERROR_CODES,
4212
+ CALENDAR_METADATA_SCHEMA_VERSION,
4213
+ CALENDAR_SYNC_STATUSES,
4214
+ CalendarClientProvider,
3753
4215
  ConsentBanner,
3754
4216
  DEFAULT_DECLINE_TTL_DAYS,
3755
4217
  DEFAULT_PHONE_COUNTRY,
3756
4218
  EVENT_SEMANTICS,
4219
+ GOOGLE_SYNC_STATES,
3757
4220
  GoogleAdsTracking,
4221
+ MAX_ATTENDEES,
4222
+ MAX_BOOKING_MINUTES,
4223
+ MAX_REMINDER_MINUTES,
4224
+ MAX_REMINDER_OVERRIDES,
3758
4225
  NAMED_RANGES,
3759
4226
  PhoneField,
3760
4227
  SUPPORTED_CURRENCIES,
3761
4228
  TRACKING_PARAM_KEYS,
3762
4229
  TRACKING_RANGES,
4230
+ bookingAttendeeSchema,
4231
+ bookingCancelSchema,
4232
+ bookingCreateSchema,
4233
+ bookingCustomerSchema,
4234
+ bookingLocationNullableSchema,
4235
+ bookingLocationSchema,
4236
+ bookingMetadataSchema,
4237
+ bookingReminderSchema,
4238
+ bookingRemindersSchema,
4239
+ bookingRescheduleSchema,
3763
4240
  captureTrackingParamsFromLocation,
3764
4241
  clearStashedUserData,
4242
+ computeSlots,
4243
+ createCalendarReadClient,
3765
4244
  createSalesClient,
3766
4245
  createTracking,
3767
4246
  createTrackingClientContext,
3768
4247
  createTrackingEventCreatePayload,
3769
4248
  createTrackingSessionUpsertPayload,
4249
+ durationMinutes,
4250
+ endOfDayInTz,
3770
4251
  fetchServices,
3771
4252
  formatDateInTz,
3772
4253
  formatMoney,
3773
4254
  formatPhone,
3774
4255
  formatPhoneAsTyped,
4256
+ formatSlotRange,
3775
4257
  fromMinor,
3776
4258
  getConsentChoice,
3777
4259
  getConsentState,
3778
4260
  getEventSemantics,
3779
4261
  getTrackingConfigRuntime,
4262
+ groupSlotsByDay,
4263
+ instantFromWallClock,
4264
+ isCalendarError,
4265
+ localDateKey,
4266
+ offsetMinutesAt,
3780
4267
  onConsentChange,
3781
4268
  optIn,
3782
4269
  optOut,
3783
4270
  parsePhone,
3784
4271
  phoneField,
3785
4272
  resetConsent,
4273
+ resolveBrowserTimeZone,
3786
4274
  resolveConversionConfig,
3787
4275
  saleCreateSchema,
3788
4276
  saleItemSchema,
@@ -3790,15 +4278,20 @@ function createTracking(options) {
3790
4278
  saleUpdateSchema,
3791
4279
  salesRequest,
3792
4280
  setConsentState,
4281
+ startOfDayInTz,
3793
4282
  stashUserData,
3794
4283
  toE164,
3795
4284
  toMinor,
4285
+ useBookingForm,
4286
+ useCalendarBusy,
4287
+ useCalendarClient,
3796
4288
  useConsent,
3797
4289
  useConsentState,
3798
4290
  useCookiePreferences,
3799
4291
  useGclid,
3800
4292
  usePhoneConfig,
3801
4293
  usePhoneField,
4294
+ useSlots,
3802
4295
  useTrackingParams
3803
4296
  });
3804
4297
  //# sourceMappingURL=index.js.map