@aranova/tracking-react 1.12.0 → 1.14.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 (36) hide show
  1. package/README.md +31 -3
  2. package/dist/{bookings-v_ucn93l.d.ts → bookings-Bw2ImtPd.d.ts} +3 -1
  3. package/dist/{bookings-Lmi0o_PA.d.mts → bookings-CDGNZUaH.d.mts} +3 -1
  4. package/dist/client.d.mts +5 -4
  5. package/dist/client.d.ts +5 -4
  6. package/dist/client.js +369 -40
  7. package/dist/client.js.map +1 -1
  8. package/dist/client.mjs +367 -39
  9. package/dist/client.mjs.map +1 -1
  10. package/dist/{form-submit-CX-CxJjQ.d.mts → form-submit-C_vJXCxh.d.mts} +11 -1
  11. package/dist/{form-submit-CX-CxJjQ.d.ts → form-submit-C_vJXCxh.d.ts} +11 -1
  12. package/dist/index.d.mts +9 -180
  13. package/dist/index.d.ts +9 -180
  14. package/dist/index.js +332 -285
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +329 -285
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/{phone-utils-D12FW8tT.d.ts → phone-utils-Cfdx474X.d.mts} +178 -3
  19. package/dist/{phone-utils-BNiSqeF7.d.mts → phone-utils-D2TGPCQW.d.ts} +178 -3
  20. package/dist/phone.d.mts +2 -2
  21. package/dist/phone.d.ts +2 -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-6vMEcopj.d.mts → sales-D4aU73mR.d.mts} +1 -1
  27. package/dist/{sales-DXLGjJQ6.d.ts → sales-DkdrGIUS.d.ts} +1 -1
  28. package/dist/sales.d.mts +3 -3
  29. package/dist/sales.d.ts +3 -3
  30. package/dist/sales.js +193 -171
  31. package/dist/sales.js.map +1 -1
  32. package/dist/sales.mjs +193 -171
  33. package/dist/sales.mjs.map +1 -1
  34. package/dist/{ingest-typed-B9I1c7jr.d.ts → submit-DuvP5AGN.d.ts} +161 -4
  35. package/dist/{ingest-typed-PKFEuJ8E.d.mts → submit-rqgXl-Wg.d.mts} +161 -4
  36. package/package.json +1 -1
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();
@@ -1614,6 +1616,10 @@ var TrackingConfigRuntime = class {
1614
1616
  integrations() {
1615
1617
  return this.current?.integrations ?? null;
1616
1618
  }
1619
+ /** The lead-form contracts, from the cached object like `integrations()`. */
1620
+ leadForms() {
1621
+ return this.current?.lead_forms ?? null;
1622
+ }
1617
1623
  async revalidateNow() {
1618
1624
  try {
1619
1625
  const headers = {};
@@ -2676,7 +2682,7 @@ function createTrackingClient(config) {
2676
2682
  }
2677
2683
 
2678
2684
  // ../tracking-core/src/ingest-typed.ts
2679
- var import_mini2 = require("zod/mini");
2685
+ var import_mini3 = require("zod/mini");
2680
2686
 
2681
2687
  // ../tracking-core/src/events/cta-click.ts
2682
2688
  var z3 = __toESM(require("zod/mini"));
@@ -2766,7 +2772,19 @@ var jsonValueSchema = z7.lazy(
2766
2772
  z7.record(z7.string(), jsonValueSchema)
2767
2773
  ])
2768
2774
  ).register(z7.globalRegistry, { id: "JsonSerializableValue" });
2769
- 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
+ });
2770
2788
  var formSubmitMetadataSchema = z7.strictObject({
2771
2789
  form: z7.strictObject({
2772
2790
  id: z7.string(),
@@ -2795,6 +2813,9 @@ var formSubmitMetadataSchema = z7.strictObject({
2795
2813
  })
2796
2814
  ).check(z7.maxLength(20))
2797
2815
  )
2816
+ ),
2817
+ contract_violations: z7.optional(
2818
+ z7.nullable(z7.array(contractViolationSchema).check(z7.maxLength(MAX_CONTRACT_VIOLATIONS)))
2798
2819
  )
2799
2820
  }),
2800
2821
  page: z7.strictObject({
@@ -3028,24 +3049,88 @@ function getEventDefinition(name) {
3028
3049
  return EVENT_REGISTRY[name];
3029
3050
  }
3030
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
+
3031
3110
  // ../tracking-core/src/ingest-typed.ts
3111
+ var clientSettings = /* @__PURE__ */ new WeakMap();
3112
+ function typedClientSettings(client) {
3113
+ return clientSettings.get(client) ?? {};
3114
+ }
3032
3115
  function createTypedClient(raw, registry, options = {}) {
3033
3116
  const debug = options.debug ?? false;
3034
3117
  if (registry.manual?.form_submit) registerCapability("form_capture");
3035
- return {
3118
+ const client = {
3036
3119
  trackEvent(eventType, metadata, opts) {
3037
3120
  const def = getEventDefinition(eventType);
3121
+ let sent = metadata;
3038
3122
  if (debug) {
3039
- (0, import_mini2.parse)(def.metadataSchema, metadata);
3040
- } 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;
3041
3126
  console.warn(
3042
- `[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`
3043
3128
  );
3044
- return;
3129
+ if (!sent) return;
3045
3130
  }
3046
3131
  raw.trackEvent({
3047
3132
  eventType,
3048
- metadata,
3133
+ metadata: sent,
3049
3134
  pageUrl: opts?.pageUrl ?? null,
3050
3135
  occurredAt: opts?.occurredAt ?? null
3051
3136
  });
@@ -3054,6 +3139,8 @@ function createTypedClient(raw, registry, options = {}) {
3054
3139
  getSessionId: raw.getSessionId.bind(raw),
3055
3140
  getVisitorId: raw.getVisitorId.bind(raw)
3056
3141
  };
3142
+ clientSettings.set(client, options);
3143
+ return client;
3057
3144
  }
3058
3145
 
3059
3146
  // ../tracking-core/src/triggers/time-on-site.ts
@@ -3124,7 +3211,7 @@ function attachSpecificPageVisit(client, config) {
3124
3211
  }
3125
3212
  const { pages } = config;
3126
3213
  const firedSet = /* @__PURE__ */ new Set();
3127
- function check() {
3214
+ function check2() {
3128
3215
  const path = window.location.pathname;
3129
3216
  for (const { name, pathPattern } of pages) {
3130
3217
  pathPattern.lastIndex = 0;
@@ -3144,20 +3231,20 @@ function attachSpecificPageVisit(client, config) {
3144
3231
  const originalReplaceState = history.replaceState.bind(history);
3145
3232
  function patchedPushState(...args) {
3146
3233
  originalPushState(...args);
3147
- setTimeout(check, 0);
3234
+ setTimeout(check2, 0);
3148
3235
  }
3149
3236
  function patchedReplaceState(...args) {
3150
3237
  originalReplaceState(...args);
3151
- setTimeout(check, 0);
3238
+ setTimeout(check2, 0);
3152
3239
  }
3153
3240
  history.pushState = patchedPushState;
3154
3241
  history.replaceState = patchedReplaceState;
3155
- window.addEventListener("popstate", check);
3156
- check();
3242
+ window.addEventListener("popstate", check2);
3243
+ check2();
3157
3244
  return () => {
3158
3245
  history.pushState = originalPushState;
3159
3246
  history.replaceState = originalReplaceState;
3160
- window.removeEventListener("popstate", check);
3247
+ window.removeEventListener("popstate", check2);
3161
3248
  };
3162
3249
  }
3163
3250
 
@@ -3260,7 +3347,7 @@ function attachScrollDepth(client, config) {
3260
3347
  }
3261
3348
  const thresholds = new Set(config.thresholds);
3262
3349
  let firedForPath = /* @__PURE__ */ new Set();
3263
- let currentPath2 = window.location.pathname;
3350
+ let currentPath3 = window.location.pathname;
3264
3351
  let rafId = null;
3265
3352
  const gate = createBaselineGate();
3266
3353
  function checkThresholds() {
@@ -3276,7 +3363,7 @@ function attachScrollDepth(client, config) {
3276
3363
  eventType: "scroll_depth",
3277
3364
  metadata: {
3278
3365
  depth_percent: threshold,
3279
- page: { path: currentPath2 }
3366
+ page: { path: currentPath3 }
3280
3367
  },
3281
3368
  pageUrl: window.location.href,
3282
3369
  occurredAt: null
@@ -3293,8 +3380,8 @@ function attachScrollDepth(client, config) {
3293
3380
  }
3294
3381
  function resetIfPathChanged() {
3295
3382
  const newPath = window.location.pathname;
3296
- if (newPath === currentPath2) return;
3297
- currentPath2 = newPath;
3383
+ if (newPath === currentPath3) return;
3384
+ currentPath3 = newPath;
3298
3385
  firedForPath = /* @__PURE__ */ new Set();
3299
3386
  gate.rebaseline();
3300
3387
  }
@@ -3356,14 +3443,14 @@ function attachMultiPageSession(client, config) {
3356
3443
  function hasFired() {
3357
3444
  return storage.getItem(FIRED_KEY) === "1";
3358
3445
  }
3359
- function check() {
3360
- const currentPath2 = window.location.pathname;
3361
- if (currentPath2 === lastCheckedPath) return;
3362
- lastCheckedPath = currentPath2;
3446
+ function check2() {
3447
+ const currentPath3 = window.location.pathname;
3448
+ if (currentPath3 === lastCheckedPath) return;
3449
+ lastCheckedPath = currentPath3;
3363
3450
  resetIfSessionChanged();
3364
3451
  if (hasFired()) return;
3365
3452
  const paths = getDistinctPaths();
3366
- paths.add(currentPath2);
3453
+ paths.add(currentPath3);
3367
3454
  saveDistinctPaths(paths);
3368
3455
  if (paths.size >= pageThreshold) {
3369
3456
  storage.setItem(FIRED_KEY, "1");
@@ -3382,20 +3469,20 @@ function attachMultiPageSession(client, config) {
3382
3469
  const originalReplaceState = history.replaceState.bind(history);
3383
3470
  function patchedPushState(...args) {
3384
3471
  originalPushState(...args);
3385
- setTimeout(check, 0);
3472
+ setTimeout(check2, 0);
3386
3473
  }
3387
3474
  function patchedReplaceState(...args) {
3388
3475
  originalReplaceState(...args);
3389
- setTimeout(check, 0);
3476
+ setTimeout(check2, 0);
3390
3477
  }
3391
3478
  history.pushState = patchedPushState;
3392
3479
  history.replaceState = patchedReplaceState;
3393
- window.addEventListener("popstate", check);
3394
- check();
3480
+ window.addEventListener("popstate", check2);
3481
+ check2();
3395
3482
  return () => {
3396
3483
  history.pushState = originalPushState;
3397
3484
  history.replaceState = originalReplaceState;
3398
- window.removeEventListener("popstate", check);
3485
+ window.removeEventListener("popstate", check2);
3399
3486
  };
3400
3487
  }
3401
3488
 
@@ -3408,7 +3495,7 @@ function attachFormStart(client, config) {
3408
3495
  }
3409
3496
  const selector = config.selector ?? "form";
3410
3497
  let firedForms = /* @__PURE__ */ new Set();
3411
- let currentPath2 = window.location.pathname;
3498
+ let currentPath3 = window.location.pathname;
3412
3499
  let pendingTap = null;
3413
3500
  function getFormKey(form) {
3414
3501
  if (form.id) return `id:${form.id}`;
@@ -3459,8 +3546,8 @@ function attachFormStart(client, config) {
3459
3546
  }
3460
3547
  function resetIfPathChanged() {
3461
3548
  const newPath = window.location.pathname;
3462
- if (newPath === currentPath2) return;
3463
- currentPath2 = newPath;
3549
+ if (newPath === currentPath3) return;
3550
+ currentPath3 = newPath;
3464
3551
  firedForms = /* @__PURE__ */ new Set();
3465
3552
  }
3466
3553
  const originalPushState = history.pushState.bind(history);
@@ -3501,7 +3588,7 @@ function attachPageExit(client) {
3501
3588
  return () => {
3502
3589
  };
3503
3590
  }
3504
- let currentPath2 = window.location.pathname;
3591
+ let currentPath3 = window.location.pathname;
3505
3592
  let activeSince = document.visibilityState === "visible" ? Date.now() : null;
3506
3593
  let accumulatedMs = 0;
3507
3594
  let maxScrollPercent = null;
@@ -3551,9 +3638,9 @@ function attachPageExit(client) {
3551
3638
  }
3552
3639
  function onNavigate() {
3553
3640
  const newPath = window.location.pathname;
3554
- if (newPath === currentPath2) return;
3555
- emitSegment(currentPath2, false);
3556
- currentPath2 = newPath;
3641
+ if (newPath === currentPath3) return;
3642
+ emitSegment(currentPath3, false);
3643
+ currentPath3 = newPath;
3557
3644
  maxScrollPercent = null;
3558
3645
  gate.rebaseline();
3559
3646
  accumulatedMs = 0;
@@ -3561,13 +3648,13 @@ function attachPageExit(client) {
3561
3648
  }
3562
3649
  function onVisibilityChange() {
3563
3650
  if (document.visibilityState === "hidden") {
3564
- emitSegment(currentPath2, true);
3651
+ emitSegment(currentPath3, true);
3565
3652
  } else {
3566
3653
  activeSince = Date.now();
3567
3654
  }
3568
3655
  }
3569
3656
  function onPageHide() {
3570
- emitSegment(currentPath2, true);
3657
+ emitSegment(currentPath3, true);
3571
3658
  }
3572
3659
  const unsubscribeNav = onHistoryChange(onNavigate);
3573
3660
  window.addEventListener("scroll", onScroll, { passive: true });
@@ -3746,6 +3833,104 @@ function attachPhoneClickCapture(client, config) {
3746
3833
  };
3747
3834
  }
3748
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
+
3749
3934
  // ../tracking-core/src/resources/sales/bookings.ts
3750
3935
  var newBookingAttemptKey = uuidV4;
3751
3936
 
@@ -4080,7 +4265,7 @@ function GoogleAdsTracking(props) {
4080
4265
  var import_react6 = require("react");
4081
4266
 
4082
4267
  // package.json
4083
- var version = "1.12.0";
4268
+ var version = "1.14.0";
4084
4269
 
4085
4270
  // ../tracking-core/src/phone-react.tsx
4086
4271
  var import_react5 = require("react");
@@ -4251,7 +4436,11 @@ function createTracking(options) {
4251
4436
  [rawClient, conversionStore]
4252
4437
  );
4253
4438
  const client = (0, import_react6.useMemo)(
4254
- () => createTypedClient(conversionClient, triggers, { debug }),
4439
+ () => createTypedClient(conversionClient, triggers, {
4440
+ debug,
4441
+ environment,
4442
+ trackingConfig
4443
+ }),
4255
4444
  [conversionClient]
4256
4445
  );
4257
4446
  (0, import_react6.useEffect)(() => {
@@ -4450,6 +4639,145 @@ function useBookingSubmit(bookings, options = {}) {
4450
4639
  const status = submitting ? "submitting" : outcome?.status ?? "idle";
4451
4640
  return { status, outcome, booked, submit, updateBooking, reset };
4452
4641
  }
4642
+
4643
+ // ../tracking-core/src/lead-forms/submit.ts
4644
+ var import_mini4 = require("zod/mini");
4645
+ var KNOWN_ROLES = new Set(LEAD_FORM_ROLES);
4646
+ var ROLE_ORDER = new Map(LEAD_FORM_ROLES.map((role, index) => [role, index]));
4647
+ var ADD_ON = /^add_on:[a-z0-9_-]{1,48}$/;
4648
+ var CONTRACT_NAME = /^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?$/;
4649
+ var INPUT_TYPES = {
4650
+ string: "text",
4651
+ text: "textarea",
4652
+ enum: "select",
4653
+ multi_enum: "select",
4654
+ bool: "checkbox",
4655
+ date: "date",
4656
+ time_window: "time",
4657
+ phone: "tel",
4658
+ email: "email",
4659
+ address: "address",
4660
+ number: "number"
4661
+ };
4662
+ function createLeadFormSubmit(tracking, contracts, options = {}) {
4663
+ registerCapability("typed_form_submit");
4664
+ const settings = { ...typedClientSettings(tracking), ...options };
4665
+ return {
4666
+ submit(input) {
4667
+ const generated = contracts.contracts[input.kind];
4668
+ const live = liveContract(settings.trackingConfig, input.kind);
4669
+ const fields = presentFields(input.fields);
4670
+ const violations = check(live === void 0 ? generated : fromLive(live), fields);
4671
+ const liveHash = live === void 0 ? void 0 : live?.contract_hash ?? null;
4672
+ if (liveHash !== void 0 && (generated?.hash ?? null) !== liveHash || contracts.registryVersion !== LEAD_FORM_REGISTRY_VERSION) {
4673
+ violations.push({ role: null, rule: "contract_behind" });
4674
+ }
4675
+ const hash = liveHash === void 0 ? generated?.hash ?? null : liveHash;
4676
+ const metadata = {
4677
+ form: {
4678
+ id: input.formId,
4679
+ action: input.action ?? null,
4680
+ fields: fields.map(([role, value]) => ({
4681
+ name: role,
4682
+ type: INPUT_TYPES[primitiveOf(role) ?? "string"],
4683
+ label: labelOf(role),
4684
+ value,
4685
+ role
4686
+ })),
4687
+ kind: input.kind,
4688
+ contract_version: LEAD_FORM_REGISTRY_VERSION,
4689
+ ...hash ? { contract_hash: hash } : {},
4690
+ ...input.extra?.length ? { extra: input.extra } : {},
4691
+ ...violations.length > 0 ? { contract_violations: violations.slice(0, MAX_CONTRACT_VIOLATIONS) } : {}
4692
+ },
4693
+ page: { path: typeof window === "undefined" ? "/" : window.location.pathname }
4694
+ };
4695
+ if (violations.length > 0 && (settings.debug || settings.environment === "development")) {
4696
+ const message = `[AranovaTracking] lead form "${input.formId}" (${input.kind}) breaks its contract: ${violations.map((v) => `${v.rule}${v.role ? ` (${v.role})` : ""}`).join(", ")}`;
4697
+ console.error(message);
4698
+ if (settings.debug) throw new Error(message);
4699
+ }
4700
+ tracking.trackEvent("form_submit", metadata);
4701
+ return metadata;
4702
+ }
4703
+ };
4704
+ }
4705
+ function liveContract(ref, kind) {
4706
+ const leadForms = ref ? getTrackingConfigRuntime(ref).leadForms() : null;
4707
+ if (!leadForms) return void 0;
4708
+ return leadForms.contracts.find((c) => c.form_kind === kind) ?? null;
4709
+ }
4710
+ function fromLive(live) {
4711
+ if (!live) return void 0;
4712
+ return {
4713
+ requires: live.requires.map((r) => ({
4714
+ role: r.role,
4715
+ requiredWhen: r.required_when ? { role: r.required_when.role, anyOf: r.required_when.any_of } : null
4716
+ })),
4717
+ requiresAny: live.requires_any.map((g) => g.roles),
4718
+ // Deprecated keys still resolve: a cached form may send them (ADR-0024 §7).
4719
+ options: Object.fromEntries(live.options.map((o) => [o.role, o.options.map((x) => x.key)]))
4720
+ };
4721
+ }
4722
+ function presentFields(fields) {
4723
+ 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));
4724
+ }
4725
+ function normalize(role, value) {
4726
+ if (typeof value !== "string") return value;
4727
+ const trimmed = value.trim();
4728
+ const primitive = primitiveOf(role);
4729
+ if (primitive === "phone") return toE164(trimmed) ?? trimmed;
4730
+ if (primitive === "email") return trimmed.toLowerCase();
4731
+ return primitive === "text" ? value : trimmed;
4732
+ }
4733
+ function isBlank(value) {
4734
+ return value === void 0 || value === null || typeof value === "string" && value.trim() === "" || Array.isArray(value) && value.length === 0;
4735
+ }
4736
+ function check(contract, fields) {
4737
+ const answered = new Map(fields);
4738
+ const violations = [];
4739
+ for (const [role, value] of fields) {
4740
+ const primitive = primitiveOf(role);
4741
+ if (!primitive) {
4742
+ violations.push({ role: CONTRACT_NAME.test(role) ? role : null, rule: "unknown_role" });
4743
+ continue;
4744
+ }
4745
+ if (!(0, import_mini4.safeParse)(leadFormPrimitiveSchemas[primitive], value).success) {
4746
+ violations.push({ role, rule: "invalid_value" });
4747
+ continue;
4748
+ }
4749
+ const allowed = contract?.options[role] ?? registryOptions(role);
4750
+ if (allowed?.length && optionKeys(value).some((key) => !allowed.includes(key))) {
4751
+ violations.push({ role, rule: "unknown_option" });
4752
+ }
4753
+ }
4754
+ for (const { role, requiredWhen } of contract?.requires ?? []) {
4755
+ const owing = requiredWhen === null || optionKeys(answered.get(requiredWhen.role)).some((key) => requiredWhen.anyOf.includes(key));
4756
+ if (owing && !answered.has(role)) violations.push({ role, rule: "missing_required" });
4757
+ }
4758
+ for (const group of contract?.requiresAny ?? []) {
4759
+ if (!group.some((role) => answered.has(role))) {
4760
+ violations.push({ role: group.join("|"), rule: "missing_any_of" });
4761
+ }
4762
+ }
4763
+ return violations;
4764
+ }
4765
+ function primitiveOf(role) {
4766
+ if (ADD_ON.test(role)) return "bool";
4767
+ return KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role].primitive : null;
4768
+ }
4769
+ function labelOf(role) {
4770
+ if (ADD_ON.test(role)) return role.slice("add_on:".length).replaceAll(/[_-]+/g, " ");
4771
+ return KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role].smsLabel || null : null;
4772
+ }
4773
+ function registryOptions(role) {
4774
+ const spec = KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role] : void 0;
4775
+ return spec && "options" in spec ? spec.options.map((o) => o.key) : void 0;
4776
+ }
4777
+ function optionKeys(value) {
4778
+ if (typeof value === "string") return [value];
4779
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
4780
+ }
4453
4781
  // Annotate the CommonJS export names for ESM import in node:
4454
4782
  0 && (module.exports = {
4455
4783
  AdPlatformTracking,
@@ -4457,6 +4785,7 @@ function useBookingSubmit(bookings, options = {}) {
4457
4785
  ConsentBanner,
4458
4786
  GoogleAdsTracking,
4459
4787
  PhoneField,
4788
+ createLeadFormSubmit,
4460
4789
  createTracking,
4461
4790
  useBookingHandoffUrl,
4462
4791
  useBookingIntegration,