@aranova/tracking-react 1.14.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.
package/dist/client.mjs CHANGED
@@ -3879,6 +3879,157 @@ var LEAD_FORM_ROLE_SPECS = {
3879
3879
  attribution: { primitive: "enum", smsLabel: "", uninteresting: true }
3880
3880
  };
3881
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
+
3882
4033
  // ../tracking-core/src/resources/sales/bookings.ts
3883
4034
  var newBookingAttemptKey = uuidV4;
3884
4035
 
@@ -4213,7 +4364,7 @@ function GoogleAdsTracking(props) {
4213
4364
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
4214
4365
 
4215
4366
  // package.json
4216
- var version = "1.14.0";
4367
+ var version = "1.14.1";
4217
4368
 
4218
4369
  // ../tracking-core/src/phone-react.tsx
4219
4370
  import {
@@ -4542,7 +4693,11 @@ function useBookingSubmit(bookings, options = {}) {
4542
4693
  const attemptKey = useRef(null);
4543
4694
  const [booked, setBooked] = useState4(null);
4544
4695
  const inFlight = useRef(null);
4545
- 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
+ );
4546
4701
  const submit = useCallback3(
4547
4702
  (input) => {
4548
4703
  if (inFlight.current) return inFlight.current;
@@ -4594,145 +4749,6 @@ function useBookingSubmit(bookings, options = {}) {
4594
4749
  const status = submitting ? "submitting" : outcome?.status ?? "idle";
4595
4750
  return { status, outcome, booked, submit, updateBooking, reset };
4596
4751
  }
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
- }
4736
4752
  export {
4737
4753
  AdPlatformTracking,
4738
4754
  BookingHandoffLink,