@aranova/tracking-react 1.13.0 → 1.14.1

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 (36) hide show
  1. package/README.md +26 -0
  2. package/dist/{bookings-C1HMPjQl.d.ts → bookings-CkfA5XOM.d.ts} +283 -8
  3. package/dist/{bookings-CHpplXU7.d.mts → bookings-DFz3BUiP.d.mts} +283 -8
  4. package/dist/client.d.mts +10 -8
  5. package/dist/client.d.ts +10 -8
  6. package/dist/client.js +382 -41
  7. package/dist/client.js.map +1 -1
  8. package/dist/client.mjs +380 -40
  9. package/dist/client.mjs.map +1 -1
  10. package/dist/index.d.mts +8 -180
  11. package/dist/index.d.ts +8 -180
  12. package/dist/index.js +375 -189
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.mjs +372 -189
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/{ingest-typed-sCDoPInT.d.ts → ingest-typed-BO6Arbw-.d.mts} +6 -99
  17. package/dist/{ingest-typed-Ckt4R731.d.mts → ingest-typed-D7I4aQ7p.d.ts} +6 -99
  18. package/dist/{phone-utils-BNiSqeF7.d.mts → phone-utils-CYr6AmEP.d.mts} +231 -3
  19. package/dist/{phone-utils-D12FW8tT.d.ts → phone-utils-CYr6AmEP.d.ts} +231 -3
  20. package/dist/phone.d.mts +1 -2
  21. package/dist/phone.d.ts +1 -2
  22. package/dist/phone.js +2 -2
  23. package/dist/phone.js.map +1 -1
  24. package/dist/phone.mjs +2 -2
  25. package/dist/phone.mjs.map +1 -1
  26. package/dist/{sales-CNvzyF5T.d.ts → sales-C0FMkN4m.d.ts} +1 -1
  27. package/dist/{sales-YUH3bQl9.d.mts → sales-KbTcowYa.d.mts} +1 -1
  28. package/dist/sales.d.mts +3 -3
  29. package/dist/sales.d.ts +3 -3
  30. package/dist/sales.js +1204 -170
  31. package/dist/sales.js.map +1 -1
  32. package/dist/sales.mjs +1204 -170
  33. package/dist/sales.mjs.map +1 -1
  34. package/package.json +1 -1
  35. package/dist/form-submit-CX-CxJjQ.d.mts +0 -47
  36. package/dist/form-submit-CX-CxJjQ.d.ts +0 -47
package/dist/client.js CHANGED
@@ -36,6 +36,7 @@ __export(client_exports, {
36
36
  ConsentBanner: () => ConsentBanner,
37
37
  GoogleAdsTracking: () => GoogleAdsTracking,
38
38
  PhoneField: () => PhoneField,
39
+ createLeadFormSubmit: () => createLeadFormSubmit,
39
40
  createTracking: () => createTracking,
40
41
  useBookingHandoffUrl: () => useBookingHandoffUrl,
41
42
  useBookingIntegration: () => useBookingIntegration,
@@ -305,7 +306,8 @@ var TRACKING_CAPABILITIES = [
305
306
  "cta_click_capture",
306
307
  "form_capture",
307
308
  "phone_click_capture",
308
- "phone_fields"
309
+ "phone_fields",
310
+ "typed_form_submit"
309
311
  ];
310
312
  var CAPABILITY_DOM_ATTRIBUTE = "data-aranova-capability";
311
313
  var registered = /* @__PURE__ */ new Set();
@@ -2680,7 +2682,7 @@ function createTrackingClient(config) {
2680
2682
  }
2681
2683
 
2682
2684
  // ../tracking-core/src/ingest-typed.ts
2683
- var import_mini2 = require("zod/mini");
2685
+ var import_mini3 = require("zod/mini");
2684
2686
 
2685
2687
  // ../tracking-core/src/events/cta-click.ts
2686
2688
  var z3 = __toESM(require("zod/mini"));
@@ -2770,7 +2772,19 @@ var jsonValueSchema = z7.lazy(
2770
2772
  z7.record(z7.string(), jsonValueSchema)
2771
2773
  ])
2772
2774
  ).register(z7.globalRegistry, { id: "JsonSerializableValue" });
2773
- var contractName = z7.string().check(z7.regex(/^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?$/));
2775
+ var contractNameSchema = z7.string().check(z7.regex(/^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?$/));
2776
+ var contractName = contractNameSchema;
2777
+ var violationRole = z7.string().check(
2778
+ z7.maxLength(200),
2779
+ z7.regex(
2780
+ /^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?(\|[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?)*$/
2781
+ )
2782
+ );
2783
+ var MAX_CONTRACT_VIOLATIONS = 50;
2784
+ var contractViolationSchema = z7.strictObject({
2785
+ role: z7.nullable(violationRole),
2786
+ rule: z7.string().check(z7.regex(/^[a-z][a-z_]{0,47}$/))
2787
+ });
2774
2788
  var formSubmitMetadataSchema = z7.strictObject({
2775
2789
  form: z7.strictObject({
2776
2790
  id: z7.string(),
@@ -2799,6 +2813,9 @@ var formSubmitMetadataSchema = z7.strictObject({
2799
2813
  })
2800
2814
  ).check(z7.maxLength(20))
2801
2815
  )
2816
+ ),
2817
+ contract_violations: z7.optional(
2818
+ z7.nullable(z7.array(contractViolationSchema).check(z7.maxLength(MAX_CONTRACT_VIOLATIONS)))
2802
2819
  )
2803
2820
  }),
2804
2821
  page: z7.strictObject({
@@ -3032,24 +3049,88 @@ function getEventDefinition(name) {
3032
3049
  return EVENT_REGISTRY[name];
3033
3050
  }
3034
3051
 
3052
+ // ../tracking-core/src/form-submit-salvage.ts
3053
+ var import_mini2 = require("zod/mini");
3054
+ var formShape = formSubmitMetadataSchema.shape.form.shape;
3055
+ var INVALID_SHAPE = { role: null, rule: "invalid_shape" };
3056
+ function salvageFormSubmit(metadata) {
3057
+ if (!isRecord2(metadata) || !isRecord2(metadata.form)) return null;
3058
+ const form = metadata.form;
3059
+ const page = isRecord2(metadata.page) ? metadata.page : {};
3060
+ const reported = Array.isArray(form.contract_violations) ? form.contract_violations.filter((v) => (0, import_mini2.safeParse)(contractViolationSchema, v).success) : [];
3061
+ const salvaged = {
3062
+ form: {
3063
+ id: typeof form.id === "string" ? form.id : String(form.id ?? ""),
3064
+ action: typeof form.action === "string" ? form.action : null,
3065
+ fields: Array.isArray(form.fields) ? form.fields.filter(isRecord2).map(salvageField) : [],
3066
+ ...valid("kind", form.kind),
3067
+ ...valid("contract_version", form.contract_version),
3068
+ ...valid("contract_hash", form.contract_hash),
3069
+ ...valid("extra", form.extra),
3070
+ contract_violations: [...reported, INVALID_SHAPE].slice(-MAX_CONTRACT_VIOLATIONS)
3071
+ },
3072
+ page: { path: typeof page.path === "string" ? page.path : currentPath2() }
3073
+ };
3074
+ const parsed = (0, import_mini2.safeParse)(formSubmitMetadataSchema, salvaged);
3075
+ return parsed.success ? parsed.data : null;
3076
+ }
3077
+ function salvageField(field) {
3078
+ return {
3079
+ name: String(field.name ?? ""),
3080
+ type: String(field.type ?? ""),
3081
+ label: typeof field.label === "string" ? field.label : null,
3082
+ value: toJson(field.value),
3083
+ ...typeof field.role === "string" && (0, import_mini2.safeParse)(contractNameSchema, field.role).success ? { role: field.role } : {}
3084
+ };
3085
+ }
3086
+ function valid(key, value) {
3087
+ if (value == null || !(0, import_mini2.safeParse)(formShape[key], value).success) return {};
3088
+ return { [key]: value };
3089
+ }
3090
+ function toJson(value) {
3091
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
3092
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
3093
+ if (typeof value === "bigint") return value.toString();
3094
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
3095
+ if (Array.isArray(value)) return value.map(toJson);
3096
+ if (isRecord2(value)) {
3097
+ return Object.fromEntries(
3098
+ Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => [k, toJson(v)])
3099
+ );
3100
+ }
3101
+ return null;
3102
+ }
3103
+ function isRecord2(value) {
3104
+ return !!value && typeof value === "object" && !Array.isArray(value);
3105
+ }
3106
+ function currentPath2() {
3107
+ return typeof window === "undefined" ? "/" : window.location.pathname;
3108
+ }
3109
+
3035
3110
  // ../tracking-core/src/ingest-typed.ts
3111
+ var clientSettings = /* @__PURE__ */ new WeakMap();
3112
+ function typedClientSettings(client) {
3113
+ return clientSettings.get(client) ?? {};
3114
+ }
3036
3115
  function createTypedClient(raw, registry, options = {}) {
3037
3116
  const debug = options.debug ?? false;
3038
3117
  if (registry.manual?.form_submit) registerCapability("form_capture");
3039
- return {
3118
+ const client = {
3040
3119
  trackEvent(eventType, metadata, opts) {
3041
3120
  const def = getEventDefinition(eventType);
3121
+ let sent = metadata;
3042
3122
  if (debug) {
3043
- (0, import_mini2.parse)(def.metadataSchema, metadata);
3044
- } else if (!(0, import_mini2.safeParse)(def.metadataSchema, metadata).success) {
3123
+ (0, import_mini3.parse)(def.metadataSchema, metadata);
3124
+ } else if (!(0, import_mini3.safeParse)(def.metadataSchema, metadata).success) {
3125
+ sent = eventType === "form_submit" ? salvageFormSubmit(metadata) : null;
3045
3126
  console.warn(
3046
- `[AranovaTracking] trackEvent: metadata for "${eventType}" failed schema validation; event dropped`
3127
+ sent ? `[AranovaTracking] trackEvent: metadata for "form_submit" failed schema validation; sent salvaged` : `[AranovaTracking] trackEvent: metadata for "${eventType}" failed schema validation; event dropped`
3047
3128
  );
3048
- return;
3129
+ if (!sent) return;
3049
3130
  }
3050
3131
  raw.trackEvent({
3051
3132
  eventType,
3052
- metadata,
3133
+ metadata: sent,
3053
3134
  pageUrl: opts?.pageUrl ?? null,
3054
3135
  occurredAt: opts?.occurredAt ?? null
3055
3136
  });
@@ -3058,6 +3139,8 @@ function createTypedClient(raw, registry, options = {}) {
3058
3139
  getSessionId: raw.getSessionId.bind(raw),
3059
3140
  getVisitorId: raw.getVisitorId.bind(raw)
3060
3141
  };
3142
+ clientSettings.set(client, options);
3143
+ return client;
3061
3144
  }
3062
3145
 
3063
3146
  // ../tracking-core/src/triggers/time-on-site.ts
@@ -3128,7 +3211,7 @@ function attachSpecificPageVisit(client, config) {
3128
3211
  }
3129
3212
  const { pages } = config;
3130
3213
  const firedSet = /* @__PURE__ */ new Set();
3131
- function check() {
3214
+ function check2() {
3132
3215
  const path = window.location.pathname;
3133
3216
  for (const { name, pathPattern } of pages) {
3134
3217
  pathPattern.lastIndex = 0;
@@ -3148,20 +3231,20 @@ function attachSpecificPageVisit(client, config) {
3148
3231
  const originalReplaceState = history.replaceState.bind(history);
3149
3232
  function patchedPushState(...args) {
3150
3233
  originalPushState(...args);
3151
- setTimeout(check, 0);
3234
+ setTimeout(check2, 0);
3152
3235
  }
3153
3236
  function patchedReplaceState(...args) {
3154
3237
  originalReplaceState(...args);
3155
- setTimeout(check, 0);
3238
+ setTimeout(check2, 0);
3156
3239
  }
3157
3240
  history.pushState = patchedPushState;
3158
3241
  history.replaceState = patchedReplaceState;
3159
- window.addEventListener("popstate", check);
3160
- check();
3242
+ window.addEventListener("popstate", check2);
3243
+ check2();
3161
3244
  return () => {
3162
3245
  history.pushState = originalPushState;
3163
3246
  history.replaceState = originalReplaceState;
3164
- window.removeEventListener("popstate", check);
3247
+ window.removeEventListener("popstate", check2);
3165
3248
  };
3166
3249
  }
3167
3250
 
@@ -3264,7 +3347,7 @@ function attachScrollDepth(client, config) {
3264
3347
  }
3265
3348
  const thresholds = new Set(config.thresholds);
3266
3349
  let firedForPath = /* @__PURE__ */ new Set();
3267
- let currentPath2 = window.location.pathname;
3350
+ let currentPath3 = window.location.pathname;
3268
3351
  let rafId = null;
3269
3352
  const gate = createBaselineGate();
3270
3353
  function checkThresholds() {
@@ -3280,7 +3363,7 @@ function attachScrollDepth(client, config) {
3280
3363
  eventType: "scroll_depth",
3281
3364
  metadata: {
3282
3365
  depth_percent: threshold,
3283
- page: { path: currentPath2 }
3366
+ page: { path: currentPath3 }
3284
3367
  },
3285
3368
  pageUrl: window.location.href,
3286
3369
  occurredAt: null
@@ -3297,8 +3380,8 @@ function attachScrollDepth(client, config) {
3297
3380
  }
3298
3381
  function resetIfPathChanged() {
3299
3382
  const newPath = window.location.pathname;
3300
- if (newPath === currentPath2) return;
3301
- currentPath2 = newPath;
3383
+ if (newPath === currentPath3) return;
3384
+ currentPath3 = newPath;
3302
3385
  firedForPath = /* @__PURE__ */ new Set();
3303
3386
  gate.rebaseline();
3304
3387
  }
@@ -3360,14 +3443,14 @@ function attachMultiPageSession(client, config) {
3360
3443
  function hasFired() {
3361
3444
  return storage.getItem(FIRED_KEY) === "1";
3362
3445
  }
3363
- function check() {
3364
- const currentPath2 = window.location.pathname;
3365
- if (currentPath2 === lastCheckedPath) return;
3366
- lastCheckedPath = currentPath2;
3446
+ function check2() {
3447
+ const currentPath3 = window.location.pathname;
3448
+ if (currentPath3 === lastCheckedPath) return;
3449
+ lastCheckedPath = currentPath3;
3367
3450
  resetIfSessionChanged();
3368
3451
  if (hasFired()) return;
3369
3452
  const paths = getDistinctPaths();
3370
- paths.add(currentPath2);
3453
+ paths.add(currentPath3);
3371
3454
  saveDistinctPaths(paths);
3372
3455
  if (paths.size >= pageThreshold) {
3373
3456
  storage.setItem(FIRED_KEY, "1");
@@ -3386,20 +3469,20 @@ function attachMultiPageSession(client, config) {
3386
3469
  const originalReplaceState = history.replaceState.bind(history);
3387
3470
  function patchedPushState(...args) {
3388
3471
  originalPushState(...args);
3389
- setTimeout(check, 0);
3472
+ setTimeout(check2, 0);
3390
3473
  }
3391
3474
  function patchedReplaceState(...args) {
3392
3475
  originalReplaceState(...args);
3393
- setTimeout(check, 0);
3476
+ setTimeout(check2, 0);
3394
3477
  }
3395
3478
  history.pushState = patchedPushState;
3396
3479
  history.replaceState = patchedReplaceState;
3397
- window.addEventListener("popstate", check);
3398
- check();
3480
+ window.addEventListener("popstate", check2);
3481
+ check2();
3399
3482
  return () => {
3400
3483
  history.pushState = originalPushState;
3401
3484
  history.replaceState = originalReplaceState;
3402
- window.removeEventListener("popstate", check);
3485
+ window.removeEventListener("popstate", check2);
3403
3486
  };
3404
3487
  }
3405
3488
 
@@ -3412,7 +3495,7 @@ function attachFormStart(client, config) {
3412
3495
  }
3413
3496
  const selector = config.selector ?? "form";
3414
3497
  let firedForms = /* @__PURE__ */ new Set();
3415
- let currentPath2 = window.location.pathname;
3498
+ let currentPath3 = window.location.pathname;
3416
3499
  let pendingTap = null;
3417
3500
  function getFormKey(form) {
3418
3501
  if (form.id) return `id:${form.id}`;
@@ -3463,8 +3546,8 @@ function attachFormStart(client, config) {
3463
3546
  }
3464
3547
  function resetIfPathChanged() {
3465
3548
  const newPath = window.location.pathname;
3466
- if (newPath === currentPath2) return;
3467
- currentPath2 = newPath;
3549
+ if (newPath === currentPath3) return;
3550
+ currentPath3 = newPath;
3468
3551
  firedForms = /* @__PURE__ */ new Set();
3469
3552
  }
3470
3553
  const originalPushState = history.pushState.bind(history);
@@ -3505,7 +3588,7 @@ function attachPageExit(client) {
3505
3588
  return () => {
3506
3589
  };
3507
3590
  }
3508
- let currentPath2 = window.location.pathname;
3591
+ let currentPath3 = window.location.pathname;
3509
3592
  let activeSince = document.visibilityState === "visible" ? Date.now() : null;
3510
3593
  let accumulatedMs = 0;
3511
3594
  let maxScrollPercent = null;
@@ -3555,9 +3638,9 @@ function attachPageExit(client) {
3555
3638
  }
3556
3639
  function onNavigate() {
3557
3640
  const newPath = window.location.pathname;
3558
- if (newPath === currentPath2) return;
3559
- emitSegment(currentPath2, false);
3560
- currentPath2 = newPath;
3641
+ if (newPath === currentPath3) return;
3642
+ emitSegment(currentPath3, false);
3643
+ currentPath3 = newPath;
3561
3644
  maxScrollPercent = null;
3562
3645
  gate.rebaseline();
3563
3646
  accumulatedMs = 0;
@@ -3565,13 +3648,13 @@ function attachPageExit(client) {
3565
3648
  }
3566
3649
  function onVisibilityChange() {
3567
3650
  if (document.visibilityState === "hidden") {
3568
- emitSegment(currentPath2, true);
3651
+ emitSegment(currentPath3, true);
3569
3652
  } else {
3570
3653
  activeSince = Date.now();
3571
3654
  }
3572
3655
  }
3573
3656
  function onPageHide() {
3574
- emitSegment(currentPath2, true);
3657
+ emitSegment(currentPath3, true);
3575
3658
  }
3576
3659
  const unsubscribeNav = onHistoryChange(onNavigate);
3577
3660
  window.addEventListener("scroll", onScroll, { passive: true });
@@ -3750,6 +3833,255 @@ function attachPhoneClickCapture(client, config) {
3750
3833
  };
3751
3834
  }
3752
3835
 
3836
+ // ../tracking-core/src/lead-forms/lead-forms.gen.ts
3837
+ var z15 = __toESM(require("zod/mini"));
3838
+ var LEAD_FORM_REGISTRY_VERSION = 2;
3839
+ var LEAD_FORM_ROLES = [
3840
+ "given_name",
3841
+ "family_name",
3842
+ "phone",
3843
+ "email",
3844
+ "service",
3845
+ "preferred_date",
3846
+ "preferred_time",
3847
+ "note",
3848
+ "visit",
3849
+ "intent",
3850
+ "vehicle",
3851
+ "drop_off",
3852
+ "address",
3853
+ "patient_type",
3854
+ "matter",
3855
+ "language",
3856
+ "products",
3857
+ "goal",
3858
+ "business_type",
3859
+ "lead_volume",
3860
+ "attribution"
3861
+ ];
3862
+ var leadFormPrimitiveSchemas = /* @__PURE__ */ (() => ({
3863
+ string: z15.string().check(z15.maxLength(120)),
3864
+ text: z15.string().check(z15.maxLength(2e3)),
3865
+ enum: z15.string().check(z15.regex(/^[a-z0-9-]{1,48}$/)),
3866
+ multi_enum: z15.array(z15.string().check(z15.regex(/^[a-z0-9-]{1,48}$/))).check(z15.maxLength(10)),
3867
+ bool: z15.boolean(),
3868
+ date: z15.string().check(z15.regex(/^\d{4}-\d{2}-\d{2}$/)),
3869
+ time_window: z15.strictObject({
3870
+ start: z15.string().check(z15.regex(/^([01]\d|2[0-3]):[0-5]\d$/)),
3871
+ end: z15.nullable(z15.string().check(z15.regex(/^([01]\d|2[0-3]):[0-5]\d$/))),
3872
+ label: z15.optional(z15.string().check(z15.maxLength(60)))
3873
+ }),
3874
+ phone: z15.string().check(z15.regex(/^\+[1-9]\d{6,14}$/)),
3875
+ email: z15.string().check(z15.maxLength(320), z15.regex(/^[^\s@]+@[^\s@]+$/)),
3876
+ address: z15.strictObject({
3877
+ line1: z15.string().check(z15.maxLength(120)),
3878
+ line2: z15.optional(z15.string().check(z15.maxLength(120))),
3879
+ city: z15.optional(z15.string().check(z15.maxLength(120))),
3880
+ region: z15.optional(z15.string().check(z15.maxLength(120))),
3881
+ postal_code: z15.optional(z15.string().check(z15.maxLength(120))),
3882
+ country: z15.optional(z15.string().check(z15.maxLength(120)))
3883
+ }),
3884
+ number: z15.number()
3885
+ }))();
3886
+ var LEAD_FORM_ROLE_SPECS = {
3887
+ given_name: { primitive: "string", smsLabel: "", contactIdentity: true },
3888
+ family_name: { primitive: "string", smsLabel: "", contactIdentity: true },
3889
+ phone: { primitive: "phone", smsLabel: "", contactIdentity: true },
3890
+ email: { primitive: "email", smsLabel: "", contactIdentity: true },
3891
+ service: { primitive: "multi_enum", smsLabel: "Service" },
3892
+ preferred_date: { primitive: "date", smsLabel: "Date" },
3893
+ preferred_time: { primitive: "time_window", smsLabel: "Time" },
3894
+ note: { primitive: "text", smsLabel: "Note" },
3895
+ visit: {
3896
+ primitive: "enum",
3897
+ smsLabel: "Visit",
3898
+ options: [
3899
+ { key: "in-shop", label: "In shop" },
3900
+ { key: "in-home", label: "In home" },
3901
+ { key: "remote", label: "Remote" },
3902
+ { key: "on-site", label: "On site" }
3903
+ ]
3904
+ },
3905
+ intent: {
3906
+ primitive: "enum",
3907
+ smsLabel: "Wants",
3908
+ options: [
3909
+ { key: "book", label: "Book" },
3910
+ { key: "quote", label: "Quote" }
3911
+ ]
3912
+ },
3913
+ vehicle: { primitive: "string", smsLabel: "Vehicle" },
3914
+ drop_off: {
3915
+ primitive: "enum",
3916
+ smsLabel: "Drop-off",
3917
+ options: [
3918
+ { key: "wait-for-it", label: "Waiting" },
3919
+ { key: "drop-off", label: "Dropping off" }
3920
+ ]
3921
+ },
3922
+ address: { primitive: "address", smsLabel: "Address" },
3923
+ patient_type: { primitive: "enum", smsLabel: "Patient" },
3924
+ matter: { primitive: "enum", smsLabel: "Matter" },
3925
+ language: { primitive: "enum", smsLabel: "Language" },
3926
+ products: { primitive: "multi_enum", smsLabel: "Products" },
3927
+ goal: { primitive: "enum", smsLabel: "Goal" },
3928
+ business_type: { primitive: "enum", smsLabel: "Business" },
3929
+ lead_volume: { primitive: "enum", smsLabel: "Lead volume" },
3930
+ add_on: { primitive: "bool", smsLabel: "" },
3931
+ attribution: { primitive: "enum", smsLabel: "", uninteresting: true }
3932
+ };
3933
+
3934
+ // ../tracking-core/src/lead-forms/submit.ts
3935
+ var import_mini4 = require("zod/mini");
3936
+ var KNOWN_ROLES = new Set(LEAD_FORM_ROLES);
3937
+ var ROLE_ORDER = new Map(LEAD_FORM_ROLES.map((role, index) => [role, index]));
3938
+ var ADD_ON = /^add_on:[a-z0-9_-]{1,48}$/;
3939
+ var CONTRACT_NAME = /^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?$/;
3940
+ var INPUT_TYPES = {
3941
+ string: "text",
3942
+ text: "textarea",
3943
+ enum: "select",
3944
+ multi_enum: "select",
3945
+ bool: "checkbox",
3946
+ date: "date",
3947
+ time_window: "time",
3948
+ phone: "tel",
3949
+ email: "email",
3950
+ address: "address",
3951
+ number: "number"
3952
+ };
3953
+ function createLeadFormSubmit(tracking, contracts, options = {}) {
3954
+ registerCapability("typed_form_submit");
3955
+ const settings = { ...typedClientSettings(tracking), ...options };
3956
+ return {
3957
+ submit(input) {
3958
+ const fields = presentFields(input.fields);
3959
+ const { violations, hash } = judgeLeadForm(
3960
+ input.kind,
3961
+ input.formId,
3962
+ fields,
3963
+ contracts,
3964
+ settings
3965
+ );
3966
+ const metadata = {
3967
+ form: {
3968
+ id: input.formId,
3969
+ action: input.action ?? null,
3970
+ fields: fields.map(([role, value]) => ({
3971
+ name: role,
3972
+ type: INPUT_TYPES[primitiveOf(role) ?? "string"],
3973
+ label: labelOf(role),
3974
+ value,
3975
+ role
3976
+ })),
3977
+ kind: input.kind,
3978
+ contract_version: LEAD_FORM_REGISTRY_VERSION,
3979
+ ...hash ? { contract_hash: hash } : {},
3980
+ ...input.extra?.length ? { extra: input.extra } : {},
3981
+ ...violations.length > 0 ? { contract_violations: violations.slice(0, MAX_CONTRACT_VIOLATIONS) } : {}
3982
+ },
3983
+ page: { path: typeof window === "undefined" ? "/" : window.location.pathname }
3984
+ };
3985
+ tracking.trackEvent("form_submit", metadata);
3986
+ return metadata;
3987
+ }
3988
+ };
3989
+ }
3990
+ function judgeLeadForm(kind, formId, fields, contracts, settings) {
3991
+ const generated = contracts?.contracts[kind];
3992
+ const live = liveContract(settings.trackingConfig, kind);
3993
+ const violations = check(live === void 0 ? generated : fromLive(live), fields);
3994
+ const liveHash = live === void 0 ? void 0 : live?.contract_hash ?? null;
3995
+ if (contracts && (liveHash !== void 0 && (generated?.hash ?? null) !== liveHash || contracts.registryVersion !== LEAD_FORM_REGISTRY_VERSION)) {
3996
+ violations.push({ role: null, rule: "contract_behind" });
3997
+ }
3998
+ if (violations.length > 0 && (settings.debug || settings.environment === "development")) {
3999
+ const message = `[AranovaTracking] lead form "${formId}" (${kind}) breaks its contract: ${violations.map((v) => `${v.rule}${v.role ? ` (${v.role})` : ""}`).join(", ")}`;
4000
+ console.error(message);
4001
+ if (settings.debug) throw new Error(message);
4002
+ }
4003
+ return {
4004
+ violations,
4005
+ hash: liveHash === void 0 ? generated?.hash ?? null : liveHash
4006
+ };
4007
+ }
4008
+ function liveContract(ref, kind) {
4009
+ const leadForms = ref ? getTrackingConfigRuntime(ref).leadForms() : null;
4010
+ if (!leadForms) return void 0;
4011
+ return leadForms.contracts.find((c) => c.form_kind === kind) ?? null;
4012
+ }
4013
+ function fromLive(live) {
4014
+ if (!live) return void 0;
4015
+ return {
4016
+ requires: live.requires.map((r) => ({
4017
+ role: r.role,
4018
+ requiredWhen: r.required_when ? { role: r.required_when.role, anyOf: r.required_when.any_of } : null
4019
+ })),
4020
+ requiresAny: live.requires_any.map((g) => g.roles),
4021
+ // Deprecated keys still resolve: a cached form may send them (ADR-0024 §7).
4022
+ options: Object.fromEntries(live.options.map((o) => [o.role, o.options.map((x) => x.key)]))
4023
+ };
4024
+ }
4025
+ function presentFields(fields) {
4026
+ return Object.entries(fields).map(([role, value]) => [role, normalize(role, value)]).filter((entry) => !isBlank(entry[1])).sort(([a], [b]) => (ROLE_ORDER.get(a) ?? 999) - (ROLE_ORDER.get(b) ?? 999));
4027
+ }
4028
+ function normalize(role, value) {
4029
+ if (typeof value !== "string") return value;
4030
+ const trimmed = value.trim();
4031
+ const primitive = primitiveOf(role);
4032
+ if (primitive === "phone") return toE164(trimmed) ?? trimmed;
4033
+ if (primitive === "email") return trimmed.toLowerCase();
4034
+ return primitive === "text" ? value : trimmed;
4035
+ }
4036
+ function isBlank(value) {
4037
+ return value === void 0 || value === null || typeof value === "string" && value.trim() === "" || Array.isArray(value) && value.length === 0;
4038
+ }
4039
+ function check(contract, fields) {
4040
+ const answered = new Map(fields);
4041
+ const violations = [];
4042
+ for (const [role, value] of fields) {
4043
+ const primitive = primitiveOf(role);
4044
+ if (!primitive) {
4045
+ violations.push({ role: CONTRACT_NAME.test(role) ? role : null, rule: "unknown_role" });
4046
+ continue;
4047
+ }
4048
+ if (!(0, import_mini4.safeParse)(leadFormPrimitiveSchemas[primitive], value).success) {
4049
+ violations.push({ role, rule: "invalid_value" });
4050
+ continue;
4051
+ }
4052
+ const allowed = contract?.options[role] ?? registryOptions(role);
4053
+ if (allowed?.length && optionKeys(value).some((key) => !allowed.includes(key))) {
4054
+ violations.push({ role, rule: "unknown_option" });
4055
+ }
4056
+ }
4057
+ for (const { role, requiredWhen } of contract?.requires ?? []) {
4058
+ const owing = requiredWhen === null || optionKeys(answered.get(requiredWhen.role)).some((key) => requiredWhen.anyOf.includes(key));
4059
+ if (owing && !answered.has(role)) violations.push({ role, rule: "missing_required" });
4060
+ }
4061
+ for (const group of contract?.requiresAny ?? []) {
4062
+ if (!group.some((role) => answered.has(role))) {
4063
+ violations.push({ role: group.join("|"), rule: "missing_any_of" });
4064
+ }
4065
+ }
4066
+ return violations;
4067
+ }
4068
+ function primitiveOf(role) {
4069
+ if (ADD_ON.test(role)) return "bool";
4070
+ return KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role].primitive : null;
4071
+ }
4072
+ function labelOf(role) {
4073
+ if (ADD_ON.test(role)) return role.slice("add_on:".length).replaceAll(/[_-]+/g, " ");
4074
+ return KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role].smsLabel || null : null;
4075
+ }
4076
+ function registryOptions(role) {
4077
+ const spec = KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role] : void 0;
4078
+ return spec && "options" in spec ? spec.options.map((o) => o.key) : void 0;
4079
+ }
4080
+ function optionKeys(value) {
4081
+ if (typeof value === "string") return [value];
4082
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
4083
+ }
4084
+
3753
4085
  // ../tracking-core/src/resources/sales/bookings.ts
3754
4086
  var newBookingAttemptKey = uuidV4;
3755
4087
 
@@ -4084,7 +4416,7 @@ function GoogleAdsTracking(props) {
4084
4416
  var import_react6 = require("react");
4085
4417
 
4086
4418
  // package.json
4087
- var version = "1.13.0";
4419
+ var version = "1.14.1";
4088
4420
 
4089
4421
  // ../tracking-core/src/phone-react.tsx
4090
4422
  var import_react5 = require("react");
@@ -4255,7 +4587,11 @@ function createTracking(options) {
4255
4587
  [rawClient, conversionStore]
4256
4588
  );
4257
4589
  const client = (0, import_react6.useMemo)(
4258
- () => createTypedClient(conversionClient, triggers, { debug }),
4590
+ () => createTypedClient(conversionClient, triggers, {
4591
+ debug,
4592
+ environment,
4593
+ trackingConfig
4594
+ }),
4259
4595
  [conversionClient]
4260
4596
  );
4261
4597
  (0, import_react6.useEffect)(() => {
@@ -4402,7 +4738,11 @@ function useBookingSubmit(bookings, options = {}) {
4402
4738
  const attemptKey = (0, import_react7.useRef)(null);
4403
4739
  const [booked, setBooked] = (0, import_react7.useState)(null);
4404
4740
  const inFlight = (0, import_react7.useRef)(null);
4405
- const { formSubmit, simulate } = options;
4741
+ const { simulate, contracts } = options;
4742
+ const formSubmit = (0, import_react7.useMemo)(
4743
+ () => options.formSubmit && contracts ? { ...options.formSubmit, contracts } : options.formSubmit,
4744
+ [options.formSubmit, contracts]
4745
+ );
4406
4746
  const submit = (0, import_react7.useCallback)(
4407
4747
  (input) => {
4408
4748
  if (inFlight.current) return inFlight.current;
@@ -4461,6 +4801,7 @@ function useBookingSubmit(bookings, options = {}) {
4461
4801
  ConsentBanner,
4462
4802
  GoogleAdsTracking,
4463
4803
  PhoneField,
4804
+ createLeadFormSubmit,
4464
4805
  createTracking,
4465
4806
  useBookingHandoffUrl,
4466
4807
  useBookingIntegration,