@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.mjs CHANGED
@@ -254,7 +254,8 @@ var TRACKING_CAPABILITIES = [
254
254
  "cta_click_capture",
255
255
  "form_capture",
256
256
  "phone_click_capture",
257
- "phone_fields"
257
+ "phone_fields",
258
+ "typed_form_submit"
258
259
  ];
259
260
  var CAPABILITY_DOM_ATTRIBUTE = "data-aranova-capability";
260
261
  var registered = /* @__PURE__ */ new Set();
@@ -2629,7 +2630,7 @@ function createTrackingClient(config) {
2629
2630
  }
2630
2631
 
2631
2632
  // ../tracking-core/src/ingest-typed.ts
2632
- import { parse as parse2, safeParse } from "zod/mini";
2633
+ import { parse as parse2, safeParse as safeParse2 } from "zod/mini";
2633
2634
 
2634
2635
  // ../tracking-core/src/events/cta-click.ts
2635
2636
  import * as z3 from "zod/mini";
@@ -2719,7 +2720,19 @@ var jsonValueSchema = z7.lazy(
2719
2720
  z7.record(z7.string(), jsonValueSchema)
2720
2721
  ])
2721
2722
  ).register(z7.globalRegistry, { id: "JsonSerializableValue" });
2722
- var contractName = z7.string().check(z7.regex(/^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?$/));
2723
+ var contractNameSchema = z7.string().check(z7.regex(/^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?$/));
2724
+ var contractName = contractNameSchema;
2725
+ var violationRole = z7.string().check(
2726
+ z7.maxLength(200),
2727
+ z7.regex(
2728
+ /^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?(\|[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?)*$/
2729
+ )
2730
+ );
2731
+ var MAX_CONTRACT_VIOLATIONS = 50;
2732
+ var contractViolationSchema = z7.strictObject({
2733
+ role: z7.nullable(violationRole),
2734
+ rule: z7.string().check(z7.regex(/^[a-z][a-z_]{0,47}$/))
2735
+ });
2723
2736
  var formSubmitMetadataSchema = z7.strictObject({
2724
2737
  form: z7.strictObject({
2725
2738
  id: z7.string(),
@@ -2748,6 +2761,9 @@ var formSubmitMetadataSchema = z7.strictObject({
2748
2761
  })
2749
2762
  ).check(z7.maxLength(20))
2750
2763
  )
2764
+ ),
2765
+ contract_violations: z7.optional(
2766
+ z7.nullable(z7.array(contractViolationSchema).check(z7.maxLength(MAX_CONTRACT_VIOLATIONS)))
2751
2767
  )
2752
2768
  }),
2753
2769
  page: z7.strictObject({
@@ -2981,24 +2997,88 @@ function getEventDefinition(name) {
2981
2997
  return EVENT_REGISTRY[name];
2982
2998
  }
2983
2999
 
3000
+ // ../tracking-core/src/form-submit-salvage.ts
3001
+ import { safeParse } from "zod/mini";
3002
+ var formShape = formSubmitMetadataSchema.shape.form.shape;
3003
+ var INVALID_SHAPE = { role: null, rule: "invalid_shape" };
3004
+ function salvageFormSubmit(metadata) {
3005
+ if (!isRecord2(metadata) || !isRecord2(metadata.form)) return null;
3006
+ const form = metadata.form;
3007
+ const page = isRecord2(metadata.page) ? metadata.page : {};
3008
+ const reported = Array.isArray(form.contract_violations) ? form.contract_violations.filter((v) => safeParse(contractViolationSchema, v).success) : [];
3009
+ const salvaged = {
3010
+ form: {
3011
+ id: typeof form.id === "string" ? form.id : String(form.id ?? ""),
3012
+ action: typeof form.action === "string" ? form.action : null,
3013
+ fields: Array.isArray(form.fields) ? form.fields.filter(isRecord2).map(salvageField) : [],
3014
+ ...valid("kind", form.kind),
3015
+ ...valid("contract_version", form.contract_version),
3016
+ ...valid("contract_hash", form.contract_hash),
3017
+ ...valid("extra", form.extra),
3018
+ contract_violations: [...reported, INVALID_SHAPE].slice(-MAX_CONTRACT_VIOLATIONS)
3019
+ },
3020
+ page: { path: typeof page.path === "string" ? page.path : currentPath2() }
3021
+ };
3022
+ const parsed = safeParse(formSubmitMetadataSchema, salvaged);
3023
+ return parsed.success ? parsed.data : null;
3024
+ }
3025
+ function salvageField(field) {
3026
+ return {
3027
+ name: String(field.name ?? ""),
3028
+ type: String(field.type ?? ""),
3029
+ label: typeof field.label === "string" ? field.label : null,
3030
+ value: toJson(field.value),
3031
+ ...typeof field.role === "string" && safeParse(contractNameSchema, field.role).success ? { role: field.role } : {}
3032
+ };
3033
+ }
3034
+ function valid(key, value) {
3035
+ if (value == null || !safeParse(formShape[key], value).success) return {};
3036
+ return { [key]: value };
3037
+ }
3038
+ function toJson(value) {
3039
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
3040
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
3041
+ if (typeof value === "bigint") return value.toString();
3042
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
3043
+ if (Array.isArray(value)) return value.map(toJson);
3044
+ if (isRecord2(value)) {
3045
+ return Object.fromEntries(
3046
+ Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => [k, toJson(v)])
3047
+ );
3048
+ }
3049
+ return null;
3050
+ }
3051
+ function isRecord2(value) {
3052
+ return !!value && typeof value === "object" && !Array.isArray(value);
3053
+ }
3054
+ function currentPath2() {
3055
+ return typeof window === "undefined" ? "/" : window.location.pathname;
3056
+ }
3057
+
2984
3058
  // ../tracking-core/src/ingest-typed.ts
3059
+ var clientSettings = /* @__PURE__ */ new WeakMap();
3060
+ function typedClientSettings(client) {
3061
+ return clientSettings.get(client) ?? {};
3062
+ }
2985
3063
  function createTypedClient(raw, registry, options = {}) {
2986
3064
  const debug = options.debug ?? false;
2987
3065
  if (registry.manual?.form_submit) registerCapability("form_capture");
2988
- return {
3066
+ const client = {
2989
3067
  trackEvent(eventType, metadata, opts) {
2990
3068
  const def = getEventDefinition(eventType);
3069
+ let sent = metadata;
2991
3070
  if (debug) {
2992
3071
  parse2(def.metadataSchema, metadata);
2993
- } else if (!safeParse(def.metadataSchema, metadata).success) {
3072
+ } else if (!safeParse2(def.metadataSchema, metadata).success) {
3073
+ sent = eventType === "form_submit" ? salvageFormSubmit(metadata) : null;
2994
3074
  console.warn(
2995
- `[AranovaTracking] trackEvent: metadata for "${eventType}" failed schema validation; event dropped`
3075
+ sent ? `[AranovaTracking] trackEvent: metadata for "form_submit" failed schema validation; sent salvaged` : `[AranovaTracking] trackEvent: metadata for "${eventType}" failed schema validation; event dropped`
2996
3076
  );
2997
- return;
3077
+ if (!sent) return;
2998
3078
  }
2999
3079
  raw.trackEvent({
3000
3080
  eventType,
3001
- metadata,
3081
+ metadata: sent,
3002
3082
  pageUrl: opts?.pageUrl ?? null,
3003
3083
  occurredAt: opts?.occurredAt ?? null
3004
3084
  });
@@ -3007,6 +3087,8 @@ function createTypedClient(raw, registry, options = {}) {
3007
3087
  getSessionId: raw.getSessionId.bind(raw),
3008
3088
  getVisitorId: raw.getVisitorId.bind(raw)
3009
3089
  };
3090
+ clientSettings.set(client, options);
3091
+ return client;
3010
3092
  }
3011
3093
 
3012
3094
  // ../tracking-core/src/triggers/time-on-site.ts
@@ -3077,7 +3159,7 @@ function attachSpecificPageVisit(client, config) {
3077
3159
  }
3078
3160
  const { pages } = config;
3079
3161
  const firedSet = /* @__PURE__ */ new Set();
3080
- function check() {
3162
+ function check2() {
3081
3163
  const path = window.location.pathname;
3082
3164
  for (const { name, pathPattern } of pages) {
3083
3165
  pathPattern.lastIndex = 0;
@@ -3097,20 +3179,20 @@ function attachSpecificPageVisit(client, config) {
3097
3179
  const originalReplaceState = history.replaceState.bind(history);
3098
3180
  function patchedPushState(...args) {
3099
3181
  originalPushState(...args);
3100
- setTimeout(check, 0);
3182
+ setTimeout(check2, 0);
3101
3183
  }
3102
3184
  function patchedReplaceState(...args) {
3103
3185
  originalReplaceState(...args);
3104
- setTimeout(check, 0);
3186
+ setTimeout(check2, 0);
3105
3187
  }
3106
3188
  history.pushState = patchedPushState;
3107
3189
  history.replaceState = patchedReplaceState;
3108
- window.addEventListener("popstate", check);
3109
- check();
3190
+ window.addEventListener("popstate", check2);
3191
+ check2();
3110
3192
  return () => {
3111
3193
  history.pushState = originalPushState;
3112
3194
  history.replaceState = originalReplaceState;
3113
- window.removeEventListener("popstate", check);
3195
+ window.removeEventListener("popstate", check2);
3114
3196
  };
3115
3197
  }
3116
3198
 
@@ -3213,7 +3295,7 @@ function attachScrollDepth(client, config) {
3213
3295
  }
3214
3296
  const thresholds = new Set(config.thresholds);
3215
3297
  let firedForPath = /* @__PURE__ */ new Set();
3216
- let currentPath2 = window.location.pathname;
3298
+ let currentPath3 = window.location.pathname;
3217
3299
  let rafId = null;
3218
3300
  const gate = createBaselineGate();
3219
3301
  function checkThresholds() {
@@ -3229,7 +3311,7 @@ function attachScrollDepth(client, config) {
3229
3311
  eventType: "scroll_depth",
3230
3312
  metadata: {
3231
3313
  depth_percent: threshold,
3232
- page: { path: currentPath2 }
3314
+ page: { path: currentPath3 }
3233
3315
  },
3234
3316
  pageUrl: window.location.href,
3235
3317
  occurredAt: null
@@ -3246,8 +3328,8 @@ function attachScrollDepth(client, config) {
3246
3328
  }
3247
3329
  function resetIfPathChanged() {
3248
3330
  const newPath = window.location.pathname;
3249
- if (newPath === currentPath2) return;
3250
- currentPath2 = newPath;
3331
+ if (newPath === currentPath3) return;
3332
+ currentPath3 = newPath;
3251
3333
  firedForPath = /* @__PURE__ */ new Set();
3252
3334
  gate.rebaseline();
3253
3335
  }
@@ -3309,14 +3391,14 @@ function attachMultiPageSession(client, config) {
3309
3391
  function hasFired() {
3310
3392
  return storage.getItem(FIRED_KEY) === "1";
3311
3393
  }
3312
- function check() {
3313
- const currentPath2 = window.location.pathname;
3314
- if (currentPath2 === lastCheckedPath) return;
3315
- lastCheckedPath = currentPath2;
3394
+ function check2() {
3395
+ const currentPath3 = window.location.pathname;
3396
+ if (currentPath3 === lastCheckedPath) return;
3397
+ lastCheckedPath = currentPath3;
3316
3398
  resetIfSessionChanged();
3317
3399
  if (hasFired()) return;
3318
3400
  const paths = getDistinctPaths();
3319
- paths.add(currentPath2);
3401
+ paths.add(currentPath3);
3320
3402
  saveDistinctPaths(paths);
3321
3403
  if (paths.size >= pageThreshold) {
3322
3404
  storage.setItem(FIRED_KEY, "1");
@@ -3335,20 +3417,20 @@ function attachMultiPageSession(client, config) {
3335
3417
  const originalReplaceState = history.replaceState.bind(history);
3336
3418
  function patchedPushState(...args) {
3337
3419
  originalPushState(...args);
3338
- setTimeout(check, 0);
3420
+ setTimeout(check2, 0);
3339
3421
  }
3340
3422
  function patchedReplaceState(...args) {
3341
3423
  originalReplaceState(...args);
3342
- setTimeout(check, 0);
3424
+ setTimeout(check2, 0);
3343
3425
  }
3344
3426
  history.pushState = patchedPushState;
3345
3427
  history.replaceState = patchedReplaceState;
3346
- window.addEventListener("popstate", check);
3347
- check();
3428
+ window.addEventListener("popstate", check2);
3429
+ check2();
3348
3430
  return () => {
3349
3431
  history.pushState = originalPushState;
3350
3432
  history.replaceState = originalReplaceState;
3351
- window.removeEventListener("popstate", check);
3433
+ window.removeEventListener("popstate", check2);
3352
3434
  };
3353
3435
  }
3354
3436
 
@@ -3361,7 +3443,7 @@ function attachFormStart(client, config) {
3361
3443
  }
3362
3444
  const selector = config.selector ?? "form";
3363
3445
  let firedForms = /* @__PURE__ */ new Set();
3364
- let currentPath2 = window.location.pathname;
3446
+ let currentPath3 = window.location.pathname;
3365
3447
  let pendingTap = null;
3366
3448
  function getFormKey(form) {
3367
3449
  if (form.id) return `id:${form.id}`;
@@ -3412,8 +3494,8 @@ function attachFormStart(client, config) {
3412
3494
  }
3413
3495
  function resetIfPathChanged() {
3414
3496
  const newPath = window.location.pathname;
3415
- if (newPath === currentPath2) return;
3416
- currentPath2 = newPath;
3497
+ if (newPath === currentPath3) return;
3498
+ currentPath3 = newPath;
3417
3499
  firedForms = /* @__PURE__ */ new Set();
3418
3500
  }
3419
3501
  const originalPushState = history.pushState.bind(history);
@@ -3454,7 +3536,7 @@ function attachPageExit(client) {
3454
3536
  return () => {
3455
3537
  };
3456
3538
  }
3457
- let currentPath2 = window.location.pathname;
3539
+ let currentPath3 = window.location.pathname;
3458
3540
  let activeSince = document.visibilityState === "visible" ? Date.now() : null;
3459
3541
  let accumulatedMs = 0;
3460
3542
  let maxScrollPercent = null;
@@ -3504,9 +3586,9 @@ function attachPageExit(client) {
3504
3586
  }
3505
3587
  function onNavigate() {
3506
3588
  const newPath = window.location.pathname;
3507
- if (newPath === currentPath2) return;
3508
- emitSegment(currentPath2, false);
3509
- currentPath2 = newPath;
3589
+ if (newPath === currentPath3) return;
3590
+ emitSegment(currentPath3, false);
3591
+ currentPath3 = newPath;
3510
3592
  maxScrollPercent = null;
3511
3593
  gate.rebaseline();
3512
3594
  accumulatedMs = 0;
@@ -3514,13 +3596,13 @@ function attachPageExit(client) {
3514
3596
  }
3515
3597
  function onVisibilityChange() {
3516
3598
  if (document.visibilityState === "hidden") {
3517
- emitSegment(currentPath2, true);
3599
+ emitSegment(currentPath3, true);
3518
3600
  } else {
3519
3601
  activeSince = Date.now();
3520
3602
  }
3521
3603
  }
3522
3604
  function onPageHide() {
3523
- emitSegment(currentPath2, true);
3605
+ emitSegment(currentPath3, true);
3524
3606
  }
3525
3607
  const unsubscribeNav = onHistoryChange(onNavigate);
3526
3608
  window.addEventListener("scroll", onScroll, { passive: true });
@@ -3699,6 +3781,255 @@ function attachPhoneClickCapture(client, config) {
3699
3781
  };
3700
3782
  }
3701
3783
 
3784
+ // ../tracking-core/src/lead-forms/lead-forms.gen.ts
3785
+ import * as z15 from "zod/mini";
3786
+ var LEAD_FORM_REGISTRY_VERSION = 2;
3787
+ var LEAD_FORM_ROLES = [
3788
+ "given_name",
3789
+ "family_name",
3790
+ "phone",
3791
+ "email",
3792
+ "service",
3793
+ "preferred_date",
3794
+ "preferred_time",
3795
+ "note",
3796
+ "visit",
3797
+ "intent",
3798
+ "vehicle",
3799
+ "drop_off",
3800
+ "address",
3801
+ "patient_type",
3802
+ "matter",
3803
+ "language",
3804
+ "products",
3805
+ "goal",
3806
+ "business_type",
3807
+ "lead_volume",
3808
+ "attribution"
3809
+ ];
3810
+ var leadFormPrimitiveSchemas = /* @__PURE__ */ (() => ({
3811
+ string: z15.string().check(z15.maxLength(120)),
3812
+ text: z15.string().check(z15.maxLength(2e3)),
3813
+ enum: z15.string().check(z15.regex(/^[a-z0-9-]{1,48}$/)),
3814
+ multi_enum: z15.array(z15.string().check(z15.regex(/^[a-z0-9-]{1,48}$/))).check(z15.maxLength(10)),
3815
+ bool: z15.boolean(),
3816
+ date: z15.string().check(z15.regex(/^\d{4}-\d{2}-\d{2}$/)),
3817
+ time_window: z15.strictObject({
3818
+ start: z15.string().check(z15.regex(/^([01]\d|2[0-3]):[0-5]\d$/)),
3819
+ end: z15.nullable(z15.string().check(z15.regex(/^([01]\d|2[0-3]):[0-5]\d$/))),
3820
+ label: z15.optional(z15.string().check(z15.maxLength(60)))
3821
+ }),
3822
+ phone: z15.string().check(z15.regex(/^\+[1-9]\d{6,14}$/)),
3823
+ email: z15.string().check(z15.maxLength(320), z15.regex(/^[^\s@]+@[^\s@]+$/)),
3824
+ address: z15.strictObject({
3825
+ line1: z15.string().check(z15.maxLength(120)),
3826
+ line2: z15.optional(z15.string().check(z15.maxLength(120))),
3827
+ city: z15.optional(z15.string().check(z15.maxLength(120))),
3828
+ region: z15.optional(z15.string().check(z15.maxLength(120))),
3829
+ postal_code: z15.optional(z15.string().check(z15.maxLength(120))),
3830
+ country: z15.optional(z15.string().check(z15.maxLength(120)))
3831
+ }),
3832
+ number: z15.number()
3833
+ }))();
3834
+ var LEAD_FORM_ROLE_SPECS = {
3835
+ given_name: { primitive: "string", smsLabel: "", contactIdentity: true },
3836
+ family_name: { primitive: "string", smsLabel: "", contactIdentity: true },
3837
+ phone: { primitive: "phone", smsLabel: "", contactIdentity: true },
3838
+ email: { primitive: "email", smsLabel: "", contactIdentity: true },
3839
+ service: { primitive: "multi_enum", smsLabel: "Service" },
3840
+ preferred_date: { primitive: "date", smsLabel: "Date" },
3841
+ preferred_time: { primitive: "time_window", smsLabel: "Time" },
3842
+ note: { primitive: "text", smsLabel: "Note" },
3843
+ visit: {
3844
+ primitive: "enum",
3845
+ smsLabel: "Visit",
3846
+ options: [
3847
+ { key: "in-shop", label: "In shop" },
3848
+ { key: "in-home", label: "In home" },
3849
+ { key: "remote", label: "Remote" },
3850
+ { key: "on-site", label: "On site" }
3851
+ ]
3852
+ },
3853
+ intent: {
3854
+ primitive: "enum",
3855
+ smsLabel: "Wants",
3856
+ options: [
3857
+ { key: "book", label: "Book" },
3858
+ { key: "quote", label: "Quote" }
3859
+ ]
3860
+ },
3861
+ vehicle: { primitive: "string", smsLabel: "Vehicle" },
3862
+ drop_off: {
3863
+ primitive: "enum",
3864
+ smsLabel: "Drop-off",
3865
+ options: [
3866
+ { key: "wait-for-it", label: "Waiting" },
3867
+ { key: "drop-off", label: "Dropping off" }
3868
+ ]
3869
+ },
3870
+ address: { primitive: "address", smsLabel: "Address" },
3871
+ patient_type: { primitive: "enum", smsLabel: "Patient" },
3872
+ matter: { primitive: "enum", smsLabel: "Matter" },
3873
+ language: { primitive: "enum", smsLabel: "Language" },
3874
+ products: { primitive: "multi_enum", smsLabel: "Products" },
3875
+ goal: { primitive: "enum", smsLabel: "Goal" },
3876
+ business_type: { primitive: "enum", smsLabel: "Business" },
3877
+ lead_volume: { primitive: "enum", smsLabel: "Lead volume" },
3878
+ add_on: { primitive: "bool", smsLabel: "" },
3879
+ attribution: { primitive: "enum", smsLabel: "", uninteresting: true }
3880
+ };
3881
+
3882
+ // ../tracking-core/src/lead-forms/submit.ts
3883
+ import { safeParse as safeParse3 } from "zod/mini";
3884
+ var KNOWN_ROLES = new Set(LEAD_FORM_ROLES);
3885
+ var ROLE_ORDER = new Map(LEAD_FORM_ROLES.map((role, index) => [role, index]));
3886
+ var ADD_ON = /^add_on:[a-z0-9_-]{1,48}$/;
3887
+ var CONTRACT_NAME = /^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?$/;
3888
+ var INPUT_TYPES = {
3889
+ string: "text",
3890
+ text: "textarea",
3891
+ enum: "select",
3892
+ multi_enum: "select",
3893
+ bool: "checkbox",
3894
+ date: "date",
3895
+ time_window: "time",
3896
+ phone: "tel",
3897
+ email: "email",
3898
+ address: "address",
3899
+ number: "number"
3900
+ };
3901
+ function createLeadFormSubmit(tracking, contracts, options = {}) {
3902
+ registerCapability("typed_form_submit");
3903
+ const settings = { ...typedClientSettings(tracking), ...options };
3904
+ return {
3905
+ submit(input) {
3906
+ const fields = presentFields(input.fields);
3907
+ const { violations, hash } = judgeLeadForm(
3908
+ input.kind,
3909
+ input.formId,
3910
+ fields,
3911
+ contracts,
3912
+ settings
3913
+ );
3914
+ const metadata = {
3915
+ form: {
3916
+ id: input.formId,
3917
+ action: input.action ?? null,
3918
+ fields: fields.map(([role, value]) => ({
3919
+ name: role,
3920
+ type: INPUT_TYPES[primitiveOf(role) ?? "string"],
3921
+ label: labelOf(role),
3922
+ value,
3923
+ role
3924
+ })),
3925
+ kind: input.kind,
3926
+ contract_version: LEAD_FORM_REGISTRY_VERSION,
3927
+ ...hash ? { contract_hash: hash } : {},
3928
+ ...input.extra?.length ? { extra: input.extra } : {},
3929
+ ...violations.length > 0 ? { contract_violations: violations.slice(0, MAX_CONTRACT_VIOLATIONS) } : {}
3930
+ },
3931
+ page: { path: typeof window === "undefined" ? "/" : window.location.pathname }
3932
+ };
3933
+ tracking.trackEvent("form_submit", metadata);
3934
+ return metadata;
3935
+ }
3936
+ };
3937
+ }
3938
+ function judgeLeadForm(kind, formId, fields, contracts, settings) {
3939
+ const generated = contracts?.contracts[kind];
3940
+ const live = liveContract(settings.trackingConfig, kind);
3941
+ const violations = check(live === void 0 ? generated : fromLive(live), fields);
3942
+ const liveHash = live === void 0 ? void 0 : live?.contract_hash ?? null;
3943
+ if (contracts && (liveHash !== void 0 && (generated?.hash ?? null) !== liveHash || contracts.registryVersion !== LEAD_FORM_REGISTRY_VERSION)) {
3944
+ violations.push({ role: null, rule: "contract_behind" });
3945
+ }
3946
+ if (violations.length > 0 && (settings.debug || settings.environment === "development")) {
3947
+ const message = `[AranovaTracking] lead form "${formId}" (${kind}) breaks its contract: ${violations.map((v) => `${v.rule}${v.role ? ` (${v.role})` : ""}`).join(", ")}`;
3948
+ console.error(message);
3949
+ if (settings.debug) throw new Error(message);
3950
+ }
3951
+ return {
3952
+ violations,
3953
+ hash: liveHash === void 0 ? generated?.hash ?? null : liveHash
3954
+ };
3955
+ }
3956
+ function liveContract(ref, kind) {
3957
+ const leadForms = ref ? getTrackingConfigRuntime(ref).leadForms() : null;
3958
+ if (!leadForms) return void 0;
3959
+ return leadForms.contracts.find((c) => c.form_kind === kind) ?? null;
3960
+ }
3961
+ function fromLive(live) {
3962
+ if (!live) return void 0;
3963
+ return {
3964
+ requires: live.requires.map((r) => ({
3965
+ role: r.role,
3966
+ requiredWhen: r.required_when ? { role: r.required_when.role, anyOf: r.required_when.any_of } : null
3967
+ })),
3968
+ requiresAny: live.requires_any.map((g) => g.roles),
3969
+ // Deprecated keys still resolve: a cached form may send them (ADR-0024 §7).
3970
+ options: Object.fromEntries(live.options.map((o) => [o.role, o.options.map((x) => x.key)]))
3971
+ };
3972
+ }
3973
+ function presentFields(fields) {
3974
+ 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));
3975
+ }
3976
+ function normalize(role, value) {
3977
+ if (typeof value !== "string") return value;
3978
+ const trimmed = value.trim();
3979
+ const primitive = primitiveOf(role);
3980
+ if (primitive === "phone") return toE164(trimmed) ?? trimmed;
3981
+ if (primitive === "email") return trimmed.toLowerCase();
3982
+ return primitive === "text" ? value : trimmed;
3983
+ }
3984
+ function isBlank(value) {
3985
+ return value === void 0 || value === null || typeof value === "string" && value.trim() === "" || Array.isArray(value) && value.length === 0;
3986
+ }
3987
+ function check(contract, fields) {
3988
+ const answered = new Map(fields);
3989
+ const violations = [];
3990
+ for (const [role, value] of fields) {
3991
+ const primitive = primitiveOf(role);
3992
+ if (!primitive) {
3993
+ violations.push({ role: CONTRACT_NAME.test(role) ? role : null, rule: "unknown_role" });
3994
+ continue;
3995
+ }
3996
+ if (!safeParse3(leadFormPrimitiveSchemas[primitive], value).success) {
3997
+ violations.push({ role, rule: "invalid_value" });
3998
+ continue;
3999
+ }
4000
+ const allowed = contract?.options[role] ?? registryOptions(role);
4001
+ if (allowed?.length && optionKeys(value).some((key) => !allowed.includes(key))) {
4002
+ violations.push({ role, rule: "unknown_option" });
4003
+ }
4004
+ }
4005
+ for (const { role, requiredWhen } of contract?.requires ?? []) {
4006
+ const owing = requiredWhen === null || optionKeys(answered.get(requiredWhen.role)).some((key) => requiredWhen.anyOf.includes(key));
4007
+ if (owing && !answered.has(role)) violations.push({ role, rule: "missing_required" });
4008
+ }
4009
+ for (const group of contract?.requiresAny ?? []) {
4010
+ if (!group.some((role) => answered.has(role))) {
4011
+ violations.push({ role: group.join("|"), rule: "missing_any_of" });
4012
+ }
4013
+ }
4014
+ return violations;
4015
+ }
4016
+ function primitiveOf(role) {
4017
+ if (ADD_ON.test(role)) return "bool";
4018
+ return KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role].primitive : null;
4019
+ }
4020
+ function labelOf(role) {
4021
+ if (ADD_ON.test(role)) return role.slice("add_on:".length).replaceAll(/[_-]+/g, " ");
4022
+ return KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role].smsLabel || null : null;
4023
+ }
4024
+ function registryOptions(role) {
4025
+ const spec = KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role] : void 0;
4026
+ return spec && "options" in spec ? spec.options.map((o) => o.key) : void 0;
4027
+ }
4028
+ function optionKeys(value) {
4029
+ if (typeof value === "string") return [value];
4030
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
4031
+ }
4032
+
3702
4033
  // ../tracking-core/src/resources/sales/bookings.ts
3703
4034
  var newBookingAttemptKey = uuidV4;
3704
4035
 
@@ -4033,7 +4364,7 @@ function GoogleAdsTracking(props) {
4033
4364
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
4034
4365
 
4035
4366
  // package.json
4036
- var version = "1.13.0";
4367
+ var version = "1.14.1";
4037
4368
 
4038
4369
  // ../tracking-core/src/phone-react.tsx
4039
4370
  import {
@@ -4211,7 +4542,11 @@ function createTracking(options) {
4211
4542
  [rawClient, conversionStore]
4212
4543
  );
4213
4544
  const client = useMemo4(
4214
- () => createTypedClient(conversionClient, triggers, { debug }),
4545
+ () => createTypedClient(conversionClient, triggers, {
4546
+ debug,
4547
+ environment,
4548
+ trackingConfig
4549
+ }),
4215
4550
  [conversionClient]
4216
4551
  );
4217
4552
  useEffect5(() => {
@@ -4358,7 +4693,11 @@ function useBookingSubmit(bookings, options = {}) {
4358
4693
  const attemptKey = useRef(null);
4359
4694
  const [booked, setBooked] = useState4(null);
4360
4695
  const inFlight = useRef(null);
4361
- const { formSubmit, simulate } = options;
4696
+ const { simulate, contracts } = options;
4697
+ const formSubmit = useMemo5(
4698
+ () => options.formSubmit && contracts ? { ...options.formSubmit, contracts } : options.formSubmit,
4699
+ [options.formSubmit, contracts]
4700
+ );
4362
4701
  const submit = useCallback3(
4363
4702
  (input) => {
4364
4703
  if (inFlight.current) return inFlight.current;
@@ -4416,6 +4755,7 @@ export {
4416
4755
  ConsentBanner,
4417
4756
  GoogleAdsTracking,
4418
4757
  PhoneField,
4758
+ createLeadFormSubmit,
4419
4759
  createTracking,
4420
4760
  useBookingHandoffUrl,
4421
4761
  useBookingIntegration,