@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.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();
@@ -1563,6 +1564,10 @@ var TrackingConfigRuntime = class {
1563
1564
  integrations() {
1564
1565
  return this.current?.integrations ?? null;
1565
1566
  }
1567
+ /** The lead-form contracts, from the cached object like `integrations()`. */
1568
+ leadForms() {
1569
+ return this.current?.lead_forms ?? null;
1570
+ }
1566
1571
  async revalidateNow() {
1567
1572
  try {
1568
1573
  const headers = {};
@@ -2625,7 +2630,7 @@ function createTrackingClient(config) {
2625
2630
  }
2626
2631
 
2627
2632
  // ../tracking-core/src/ingest-typed.ts
2628
- import { parse as parse2, safeParse } from "zod/mini";
2633
+ import { parse as parse2, safeParse as safeParse2 } from "zod/mini";
2629
2634
 
2630
2635
  // ../tracking-core/src/events/cta-click.ts
2631
2636
  import * as z3 from "zod/mini";
@@ -2715,7 +2720,19 @@ var jsonValueSchema = z7.lazy(
2715
2720
  z7.record(z7.string(), jsonValueSchema)
2716
2721
  ])
2717
2722
  ).register(z7.globalRegistry, { id: "JsonSerializableValue" });
2718
- 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
+ });
2719
2736
  var formSubmitMetadataSchema = z7.strictObject({
2720
2737
  form: z7.strictObject({
2721
2738
  id: z7.string(),
@@ -2744,6 +2761,9 @@ var formSubmitMetadataSchema = z7.strictObject({
2744
2761
  })
2745
2762
  ).check(z7.maxLength(20))
2746
2763
  )
2764
+ ),
2765
+ contract_violations: z7.optional(
2766
+ z7.nullable(z7.array(contractViolationSchema).check(z7.maxLength(MAX_CONTRACT_VIOLATIONS)))
2747
2767
  )
2748
2768
  }),
2749
2769
  page: z7.strictObject({
@@ -2977,24 +2997,88 @@ function getEventDefinition(name) {
2977
2997
  return EVENT_REGISTRY[name];
2978
2998
  }
2979
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
+
2980
3058
  // ../tracking-core/src/ingest-typed.ts
3059
+ var clientSettings = /* @__PURE__ */ new WeakMap();
3060
+ function typedClientSettings(client) {
3061
+ return clientSettings.get(client) ?? {};
3062
+ }
2981
3063
  function createTypedClient(raw, registry, options = {}) {
2982
3064
  const debug = options.debug ?? false;
2983
3065
  if (registry.manual?.form_submit) registerCapability("form_capture");
2984
- return {
3066
+ const client = {
2985
3067
  trackEvent(eventType, metadata, opts) {
2986
3068
  const def = getEventDefinition(eventType);
3069
+ let sent = metadata;
2987
3070
  if (debug) {
2988
3071
  parse2(def.metadataSchema, metadata);
2989
- } else if (!safeParse(def.metadataSchema, metadata).success) {
3072
+ } else if (!safeParse2(def.metadataSchema, metadata).success) {
3073
+ sent = eventType === "form_submit" ? salvageFormSubmit(metadata) : null;
2990
3074
  console.warn(
2991
- `[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`
2992
3076
  );
2993
- return;
3077
+ if (!sent) return;
2994
3078
  }
2995
3079
  raw.trackEvent({
2996
3080
  eventType,
2997
- metadata,
3081
+ metadata: sent,
2998
3082
  pageUrl: opts?.pageUrl ?? null,
2999
3083
  occurredAt: opts?.occurredAt ?? null
3000
3084
  });
@@ -3003,6 +3087,8 @@ function createTypedClient(raw, registry, options = {}) {
3003
3087
  getSessionId: raw.getSessionId.bind(raw),
3004
3088
  getVisitorId: raw.getVisitorId.bind(raw)
3005
3089
  };
3090
+ clientSettings.set(client, options);
3091
+ return client;
3006
3092
  }
3007
3093
 
3008
3094
  // ../tracking-core/src/triggers/time-on-site.ts
@@ -3073,7 +3159,7 @@ function attachSpecificPageVisit(client, config) {
3073
3159
  }
3074
3160
  const { pages } = config;
3075
3161
  const firedSet = /* @__PURE__ */ new Set();
3076
- function check() {
3162
+ function check2() {
3077
3163
  const path = window.location.pathname;
3078
3164
  for (const { name, pathPattern } of pages) {
3079
3165
  pathPattern.lastIndex = 0;
@@ -3093,20 +3179,20 @@ function attachSpecificPageVisit(client, config) {
3093
3179
  const originalReplaceState = history.replaceState.bind(history);
3094
3180
  function patchedPushState(...args) {
3095
3181
  originalPushState(...args);
3096
- setTimeout(check, 0);
3182
+ setTimeout(check2, 0);
3097
3183
  }
3098
3184
  function patchedReplaceState(...args) {
3099
3185
  originalReplaceState(...args);
3100
- setTimeout(check, 0);
3186
+ setTimeout(check2, 0);
3101
3187
  }
3102
3188
  history.pushState = patchedPushState;
3103
3189
  history.replaceState = patchedReplaceState;
3104
- window.addEventListener("popstate", check);
3105
- check();
3190
+ window.addEventListener("popstate", check2);
3191
+ check2();
3106
3192
  return () => {
3107
3193
  history.pushState = originalPushState;
3108
3194
  history.replaceState = originalReplaceState;
3109
- window.removeEventListener("popstate", check);
3195
+ window.removeEventListener("popstate", check2);
3110
3196
  };
3111
3197
  }
3112
3198
 
@@ -3209,7 +3295,7 @@ function attachScrollDepth(client, config) {
3209
3295
  }
3210
3296
  const thresholds = new Set(config.thresholds);
3211
3297
  let firedForPath = /* @__PURE__ */ new Set();
3212
- let currentPath2 = window.location.pathname;
3298
+ let currentPath3 = window.location.pathname;
3213
3299
  let rafId = null;
3214
3300
  const gate = createBaselineGate();
3215
3301
  function checkThresholds() {
@@ -3225,7 +3311,7 @@ function attachScrollDepth(client, config) {
3225
3311
  eventType: "scroll_depth",
3226
3312
  metadata: {
3227
3313
  depth_percent: threshold,
3228
- page: { path: currentPath2 }
3314
+ page: { path: currentPath3 }
3229
3315
  },
3230
3316
  pageUrl: window.location.href,
3231
3317
  occurredAt: null
@@ -3242,8 +3328,8 @@ function attachScrollDepth(client, config) {
3242
3328
  }
3243
3329
  function resetIfPathChanged() {
3244
3330
  const newPath = window.location.pathname;
3245
- if (newPath === currentPath2) return;
3246
- currentPath2 = newPath;
3331
+ if (newPath === currentPath3) return;
3332
+ currentPath3 = newPath;
3247
3333
  firedForPath = /* @__PURE__ */ new Set();
3248
3334
  gate.rebaseline();
3249
3335
  }
@@ -3305,14 +3391,14 @@ function attachMultiPageSession(client, config) {
3305
3391
  function hasFired() {
3306
3392
  return storage.getItem(FIRED_KEY) === "1";
3307
3393
  }
3308
- function check() {
3309
- const currentPath2 = window.location.pathname;
3310
- if (currentPath2 === lastCheckedPath) return;
3311
- lastCheckedPath = currentPath2;
3394
+ function check2() {
3395
+ const currentPath3 = window.location.pathname;
3396
+ if (currentPath3 === lastCheckedPath) return;
3397
+ lastCheckedPath = currentPath3;
3312
3398
  resetIfSessionChanged();
3313
3399
  if (hasFired()) return;
3314
3400
  const paths = getDistinctPaths();
3315
- paths.add(currentPath2);
3401
+ paths.add(currentPath3);
3316
3402
  saveDistinctPaths(paths);
3317
3403
  if (paths.size >= pageThreshold) {
3318
3404
  storage.setItem(FIRED_KEY, "1");
@@ -3331,20 +3417,20 @@ function attachMultiPageSession(client, config) {
3331
3417
  const originalReplaceState = history.replaceState.bind(history);
3332
3418
  function patchedPushState(...args) {
3333
3419
  originalPushState(...args);
3334
- setTimeout(check, 0);
3420
+ setTimeout(check2, 0);
3335
3421
  }
3336
3422
  function patchedReplaceState(...args) {
3337
3423
  originalReplaceState(...args);
3338
- setTimeout(check, 0);
3424
+ setTimeout(check2, 0);
3339
3425
  }
3340
3426
  history.pushState = patchedPushState;
3341
3427
  history.replaceState = patchedReplaceState;
3342
- window.addEventListener("popstate", check);
3343
- check();
3428
+ window.addEventListener("popstate", check2);
3429
+ check2();
3344
3430
  return () => {
3345
3431
  history.pushState = originalPushState;
3346
3432
  history.replaceState = originalReplaceState;
3347
- window.removeEventListener("popstate", check);
3433
+ window.removeEventListener("popstate", check2);
3348
3434
  };
3349
3435
  }
3350
3436
 
@@ -3357,7 +3443,7 @@ function attachFormStart(client, config) {
3357
3443
  }
3358
3444
  const selector = config.selector ?? "form";
3359
3445
  let firedForms = /* @__PURE__ */ new Set();
3360
- let currentPath2 = window.location.pathname;
3446
+ let currentPath3 = window.location.pathname;
3361
3447
  let pendingTap = null;
3362
3448
  function getFormKey(form) {
3363
3449
  if (form.id) return `id:${form.id}`;
@@ -3408,8 +3494,8 @@ function attachFormStart(client, config) {
3408
3494
  }
3409
3495
  function resetIfPathChanged() {
3410
3496
  const newPath = window.location.pathname;
3411
- if (newPath === currentPath2) return;
3412
- currentPath2 = newPath;
3497
+ if (newPath === currentPath3) return;
3498
+ currentPath3 = newPath;
3413
3499
  firedForms = /* @__PURE__ */ new Set();
3414
3500
  }
3415
3501
  const originalPushState = history.pushState.bind(history);
@@ -3450,7 +3536,7 @@ function attachPageExit(client) {
3450
3536
  return () => {
3451
3537
  };
3452
3538
  }
3453
- let currentPath2 = window.location.pathname;
3539
+ let currentPath3 = window.location.pathname;
3454
3540
  let activeSince = document.visibilityState === "visible" ? Date.now() : null;
3455
3541
  let accumulatedMs = 0;
3456
3542
  let maxScrollPercent = null;
@@ -3500,9 +3586,9 @@ function attachPageExit(client) {
3500
3586
  }
3501
3587
  function onNavigate() {
3502
3588
  const newPath = window.location.pathname;
3503
- if (newPath === currentPath2) return;
3504
- emitSegment(currentPath2, false);
3505
- currentPath2 = newPath;
3589
+ if (newPath === currentPath3) return;
3590
+ emitSegment(currentPath3, false);
3591
+ currentPath3 = newPath;
3506
3592
  maxScrollPercent = null;
3507
3593
  gate.rebaseline();
3508
3594
  accumulatedMs = 0;
@@ -3510,13 +3596,13 @@ function attachPageExit(client) {
3510
3596
  }
3511
3597
  function onVisibilityChange() {
3512
3598
  if (document.visibilityState === "hidden") {
3513
- emitSegment(currentPath2, true);
3599
+ emitSegment(currentPath3, true);
3514
3600
  } else {
3515
3601
  activeSince = Date.now();
3516
3602
  }
3517
3603
  }
3518
3604
  function onPageHide() {
3519
- emitSegment(currentPath2, true);
3605
+ emitSegment(currentPath3, true);
3520
3606
  }
3521
3607
  const unsubscribeNav = onHistoryChange(onNavigate);
3522
3608
  window.addEventListener("scroll", onScroll, { passive: true });
@@ -3695,6 +3781,104 @@ function attachPhoneClickCapture(client, config) {
3695
3781
  };
3696
3782
  }
3697
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
+
3698
3882
  // ../tracking-core/src/resources/sales/bookings.ts
3699
3883
  var newBookingAttemptKey = uuidV4;
3700
3884
 
@@ -4029,7 +4213,7 @@ function GoogleAdsTracking(props) {
4029
4213
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
4030
4214
 
4031
4215
  // package.json
4032
- var version = "1.12.0";
4216
+ var version = "1.14.0";
4033
4217
 
4034
4218
  // ../tracking-core/src/phone-react.tsx
4035
4219
  import {
@@ -4207,7 +4391,11 @@ function createTracking(options) {
4207
4391
  [rawClient, conversionStore]
4208
4392
  );
4209
4393
  const client = useMemo4(
4210
- () => createTypedClient(conversionClient, triggers, { debug }),
4394
+ () => createTypedClient(conversionClient, triggers, {
4395
+ debug,
4396
+ environment,
4397
+ trackingConfig
4398
+ }),
4211
4399
  [conversionClient]
4212
4400
  );
4213
4401
  useEffect5(() => {
@@ -4406,12 +4594,152 @@ function useBookingSubmit(bookings, options = {}) {
4406
4594
  const status = submitting ? "submitting" : outcome?.status ?? "idle";
4407
4595
  return { status, outcome, booked, submit, updateBooking, reset };
4408
4596
  }
4597
+
4598
+ // ../tracking-core/src/lead-forms/submit.ts
4599
+ import { safeParse as safeParse3 } from "zod/mini";
4600
+ var KNOWN_ROLES = new Set(LEAD_FORM_ROLES);
4601
+ var ROLE_ORDER = new Map(LEAD_FORM_ROLES.map((role, index) => [role, index]));
4602
+ var ADD_ON = /^add_on:[a-z0-9_-]{1,48}$/;
4603
+ var CONTRACT_NAME = /^[a-z][a-z0-9_]{0,47}(:[a-z0-9_-]{1,48})?$/;
4604
+ var INPUT_TYPES = {
4605
+ string: "text",
4606
+ text: "textarea",
4607
+ enum: "select",
4608
+ multi_enum: "select",
4609
+ bool: "checkbox",
4610
+ date: "date",
4611
+ time_window: "time",
4612
+ phone: "tel",
4613
+ email: "email",
4614
+ address: "address",
4615
+ number: "number"
4616
+ };
4617
+ function createLeadFormSubmit(tracking, contracts, options = {}) {
4618
+ registerCapability("typed_form_submit");
4619
+ const settings = { ...typedClientSettings(tracking), ...options };
4620
+ return {
4621
+ submit(input) {
4622
+ const generated = contracts.contracts[input.kind];
4623
+ const live = liveContract(settings.trackingConfig, input.kind);
4624
+ const fields = presentFields(input.fields);
4625
+ const violations = check(live === void 0 ? generated : fromLive(live), fields);
4626
+ const liveHash = live === void 0 ? void 0 : live?.contract_hash ?? null;
4627
+ if (liveHash !== void 0 && (generated?.hash ?? null) !== liveHash || contracts.registryVersion !== LEAD_FORM_REGISTRY_VERSION) {
4628
+ violations.push({ role: null, rule: "contract_behind" });
4629
+ }
4630
+ const hash = liveHash === void 0 ? generated?.hash ?? null : liveHash;
4631
+ const metadata = {
4632
+ form: {
4633
+ id: input.formId,
4634
+ action: input.action ?? null,
4635
+ fields: fields.map(([role, value]) => ({
4636
+ name: role,
4637
+ type: INPUT_TYPES[primitiveOf(role) ?? "string"],
4638
+ label: labelOf(role),
4639
+ value,
4640
+ role
4641
+ })),
4642
+ kind: input.kind,
4643
+ contract_version: LEAD_FORM_REGISTRY_VERSION,
4644
+ ...hash ? { contract_hash: hash } : {},
4645
+ ...input.extra?.length ? { extra: input.extra } : {},
4646
+ ...violations.length > 0 ? { contract_violations: violations.slice(0, MAX_CONTRACT_VIOLATIONS) } : {}
4647
+ },
4648
+ page: { path: typeof window === "undefined" ? "/" : window.location.pathname }
4649
+ };
4650
+ if (violations.length > 0 && (settings.debug || settings.environment === "development")) {
4651
+ const message = `[AranovaTracking] lead form "${input.formId}" (${input.kind}) breaks its contract: ${violations.map((v) => `${v.rule}${v.role ? ` (${v.role})` : ""}`).join(", ")}`;
4652
+ console.error(message);
4653
+ if (settings.debug) throw new Error(message);
4654
+ }
4655
+ tracking.trackEvent("form_submit", metadata);
4656
+ return metadata;
4657
+ }
4658
+ };
4659
+ }
4660
+ function liveContract(ref, kind) {
4661
+ const leadForms = ref ? getTrackingConfigRuntime(ref).leadForms() : null;
4662
+ if (!leadForms) return void 0;
4663
+ return leadForms.contracts.find((c) => c.form_kind === kind) ?? null;
4664
+ }
4665
+ function fromLive(live) {
4666
+ if (!live) return void 0;
4667
+ return {
4668
+ requires: live.requires.map((r) => ({
4669
+ role: r.role,
4670
+ requiredWhen: r.required_when ? { role: r.required_when.role, anyOf: r.required_when.any_of } : null
4671
+ })),
4672
+ requiresAny: live.requires_any.map((g) => g.roles),
4673
+ // Deprecated keys still resolve: a cached form may send them (ADR-0024 §7).
4674
+ options: Object.fromEntries(live.options.map((o) => [o.role, o.options.map((x) => x.key)]))
4675
+ };
4676
+ }
4677
+ function presentFields(fields) {
4678
+ 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));
4679
+ }
4680
+ function normalize(role, value) {
4681
+ if (typeof value !== "string") return value;
4682
+ const trimmed = value.trim();
4683
+ const primitive = primitiveOf(role);
4684
+ if (primitive === "phone") return toE164(trimmed) ?? trimmed;
4685
+ if (primitive === "email") return trimmed.toLowerCase();
4686
+ return primitive === "text" ? value : trimmed;
4687
+ }
4688
+ function isBlank(value) {
4689
+ return value === void 0 || value === null || typeof value === "string" && value.trim() === "" || Array.isArray(value) && value.length === 0;
4690
+ }
4691
+ function check(contract, fields) {
4692
+ const answered = new Map(fields);
4693
+ const violations = [];
4694
+ for (const [role, value] of fields) {
4695
+ const primitive = primitiveOf(role);
4696
+ if (!primitive) {
4697
+ violations.push({ role: CONTRACT_NAME.test(role) ? role : null, rule: "unknown_role" });
4698
+ continue;
4699
+ }
4700
+ if (!safeParse3(leadFormPrimitiveSchemas[primitive], value).success) {
4701
+ violations.push({ role, rule: "invalid_value" });
4702
+ continue;
4703
+ }
4704
+ const allowed = contract?.options[role] ?? registryOptions(role);
4705
+ if (allowed?.length && optionKeys(value).some((key) => !allowed.includes(key))) {
4706
+ violations.push({ role, rule: "unknown_option" });
4707
+ }
4708
+ }
4709
+ for (const { role, requiredWhen } of contract?.requires ?? []) {
4710
+ const owing = requiredWhen === null || optionKeys(answered.get(requiredWhen.role)).some((key) => requiredWhen.anyOf.includes(key));
4711
+ if (owing && !answered.has(role)) violations.push({ role, rule: "missing_required" });
4712
+ }
4713
+ for (const group of contract?.requiresAny ?? []) {
4714
+ if (!group.some((role) => answered.has(role))) {
4715
+ violations.push({ role: group.join("|"), rule: "missing_any_of" });
4716
+ }
4717
+ }
4718
+ return violations;
4719
+ }
4720
+ function primitiveOf(role) {
4721
+ if (ADD_ON.test(role)) return "bool";
4722
+ return KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role].primitive : null;
4723
+ }
4724
+ function labelOf(role) {
4725
+ if (ADD_ON.test(role)) return role.slice("add_on:".length).replaceAll(/[_-]+/g, " ");
4726
+ return KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role].smsLabel || null : null;
4727
+ }
4728
+ function registryOptions(role) {
4729
+ const spec = KNOWN_ROLES.has(role) ? LEAD_FORM_ROLE_SPECS[role] : void 0;
4730
+ return spec && "options" in spec ? spec.options.map((o) => o.key) : void 0;
4731
+ }
4732
+ function optionKeys(value) {
4733
+ if (typeof value === "string") return [value];
4734
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
4735
+ }
4409
4736
  export {
4410
4737
  AdPlatformTracking,
4411
4738
  BookingHandoffLink,
4412
4739
  ConsentBanner,
4413
4740
  GoogleAdsTracking,
4414
4741
  PhoneField,
4742
+ createLeadFormSubmit,
4415
4743
  createTracking,
4416
4744
  useBookingHandoffUrl,
4417
4745
  useBookingIntegration,