@aranova/tracking-react 0.18.0 → 0.19.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.
package/dist/index.mjs CHANGED
@@ -4,6 +4,140 @@ import { useEffect as useEffect2, useState as useState2 } from "react";
4
4
  // src/hooks.ts
5
5
  import { useCallback, useEffect, useState } from "react";
6
6
 
7
+ // ../tracking-core/src/phone.ts
8
+ import { AsYouType, parsePhoneNumberFromString } from "libphonenumber-js";
9
+ var DEFAULT_PHONE_COUNTRY = "CA";
10
+ function parsePhone(raw, country) {
11
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
12
+ const parsed = parsePhoneNumberFromString(raw ?? "", region);
13
+ if (!parsed) {
14
+ return { e164: null, national: "", international: "", country: region, isValid: false };
15
+ }
16
+ const isValid = parsed.isValid();
17
+ return {
18
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
19
+ // (e.g. too few digits) still parses but must not be transmitted.
20
+ e164: isValid ? parsed.number : null,
21
+ national: parsed.formatNational(),
22
+ international: parsed.formatInternational(),
23
+ country: parsed.country ?? region,
24
+ isValid
25
+ };
26
+ }
27
+ function toE164(raw, country) {
28
+ return parsePhone(raw, country).e164;
29
+ }
30
+ function formatPhone(value, format = "national", country) {
31
+ const parsed = parsePhone(value, country);
32
+ if (typeof format === "function") return format(parsed);
33
+ switch (format) {
34
+ case "international":
35
+ return parsed.international || value;
36
+ case "e164":
37
+ return parsed.e164 ?? value;
38
+ case "national":
39
+ default:
40
+ return parsed.national || value;
41
+ }
42
+ }
43
+ function formatPhoneAsTyped(raw, country) {
44
+ return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
45
+ }
46
+
47
+ // ../tracking-core/src/user-data.ts
48
+ var EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
49
+ var EMAIL_NAME_HINT = /e[-_]?mail/i;
50
+ var PHONE_NAME_HINT = /(^|[^a-z])(phone|tel|mobile|cell)/i;
51
+ var stash = { email: null, phoneNumber: null };
52
+ function normalizeEmail(raw) {
53
+ if (typeof raw !== "string") return null;
54
+ const cleaned = raw.trim().toLowerCase();
55
+ return EMAIL_SHAPE.test(cleaned) ? cleaned : null;
56
+ }
57
+ function fieldText(field, key) {
58
+ const value = field[key];
59
+ return typeof value === "string" ? value : "";
60
+ }
61
+ function extractUserDataFromFormFields(fields, country) {
62
+ const result = { email: null, phoneNumber: null };
63
+ if (!Array.isArray(fields)) return result;
64
+ const passes = [
65
+ (field, _hint, type) => fieldText(field, "type").toLowerCase() === type,
66
+ (field, hint) => hint.test(fieldText(field, "name")) || hint.test(fieldText(field, "label"))
67
+ ];
68
+ for (const matches of passes) {
69
+ for (const raw of fields) {
70
+ if (raw === null || typeof raw !== "object") continue;
71
+ const field = raw;
72
+ if (typeof field.value !== "string" || field.value.length === 0) continue;
73
+ if (result.email === null && matches(field, EMAIL_NAME_HINT, "email")) {
74
+ result.email = normalizeEmail(field.value);
75
+ }
76
+ if (result.phoneNumber === null && matches(field, PHONE_NAME_HINT, "tel")) {
77
+ try {
78
+ result.phoneNumber = toE164(field.value, country);
79
+ } catch {
80
+ }
81
+ }
82
+ }
83
+ if (result.email !== null && result.phoneNumber !== null) break;
84
+ }
85
+ return result;
86
+ }
87
+ function stashUserData(data, country) {
88
+ const email = normalizeEmail(data.email);
89
+ let phoneNumber = null;
90
+ if (typeof data.phone === "string" && data.phone.length > 0) {
91
+ try {
92
+ phoneNumber = toE164(data.phone, country);
93
+ } catch {
94
+ phoneNumber = null;
95
+ }
96
+ }
97
+ stash = {
98
+ email: email ?? stash.email,
99
+ phoneNumber: phoneNumber ?? stash.phoneNumber
100
+ };
101
+ }
102
+ function stashUserDataFromFormFields(fields, country) {
103
+ try {
104
+ const extracted = extractUserDataFromFormFields(fields, country);
105
+ stash = {
106
+ email: extracted.email ?? stash.email,
107
+ phoneNumber: extracted.phoneNumber ?? stash.phoneNumber
108
+ };
109
+ } catch {
110
+ }
111
+ }
112
+ function clearStashedUserData() {
113
+ stash = { email: null, phoneNumber: null };
114
+ }
115
+ function applyUserDataForConversion(explicit, country) {
116
+ if (typeof window === "undefined" || typeof window.gtag !== "function") return false;
117
+ let email = stash.email;
118
+ let phoneNumber = stash.phoneNumber;
119
+ if (explicit) {
120
+ const normalizedEmail = normalizeEmail(explicit.email);
121
+ if (normalizedEmail) email = normalizedEmail;
122
+ if (typeof explicit.phone === "string" && explicit.phone.length > 0) {
123
+ try {
124
+ phoneNumber = toE164(explicit.phone, country) ?? phoneNumber;
125
+ } catch {
126
+ }
127
+ }
128
+ }
129
+ if (email === null && phoneNumber === null) return false;
130
+ try {
131
+ window.gtag("set", "user_data", {
132
+ ...email !== null ? { email } : {},
133
+ ...phoneNumber !== null ? { phone_number: phoneNumber } : {}
134
+ });
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
7
141
  // ../tracking-core/src/consent.ts
8
142
  var CONSENT_STATE_KEY = "consent_state";
9
143
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
@@ -89,6 +223,13 @@ function setConsentState(state, options) {
89
223
  } catch {
90
224
  }
91
225
  pushConsentToPlatforms(state);
226
+ if (state === "denied") {
227
+ clearStashedUserData();
228
+ try {
229
+ if (typeof window.gtag === "function") window.gtag("set", "user_data", null);
230
+ } catch {
231
+ }
232
+ }
92
233
  notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
93
234
  }
94
235
  function optIn() {
@@ -113,6 +254,11 @@ function resetConsent() {
113
254
  var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
114
255
  var TRACKING_PARAM_KEYS = [
115
256
  "gclid",
257
+ // Google's iOS/Safari replacement click IDs — issued when privacy features
258
+ // withhold gclid (wbraid: web-to-web, gbraid: app-to-web). First-class
259
+ // citizens: captured, persisted, and attributed exactly like gclid.
260
+ "wbraid",
261
+ "gbraid",
116
262
  "fbclid",
117
263
  "utm_source",
118
264
  "utm_medium",
@@ -123,6 +269,8 @@ var TRACKING_PARAM_KEYS = [
123
269
  function createEmptyTrackingParams() {
124
270
  return {
125
271
  gclid: null,
272
+ wbraid: null,
273
+ gbraid: null,
126
274
  fbclid: null,
127
275
  utm_source: null,
128
276
  utm_medium: null,
@@ -454,6 +602,8 @@ function buildLandingPayloadFields(sessionId, override) {
454
602
  if (params === null) return {};
455
603
  return {
456
604
  landing_gclid: params.gclid ?? null,
605
+ landing_wbraid: params.wbraid ?? null,
606
+ landing_gbraid: params.gbraid ?? null,
457
607
  landing_fbclid: params.fbclid ?? null,
458
608
  landing_utm_source: params.utm_source ?? null,
459
609
  landing_utm_medium: params.utm_medium ?? null,
@@ -481,6 +631,8 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
481
631
  session_id: input.sessionId,
482
632
  visitor_id: input.visitorId ?? null,
483
633
  gclid: trackingParams.gclid,
634
+ wbraid: trackingParams.wbraid,
635
+ gbraid: trackingParams.gbraid,
484
636
  fbclid: trackingParams.fbclid,
485
637
  fbc: getFbcCookie(),
486
638
  fbp: getFbpCookie(),
@@ -502,6 +654,8 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
502
654
  session_id: input.sessionId,
503
655
  event_type: input.eventType,
504
656
  gclid: trackingParams.gclid,
657
+ wbraid: trackingParams.wbraid,
658
+ gbraid: trackingParams.gbraid,
505
659
  fbclid: trackingParams.fbclid,
506
660
  fbc: getFbcCookie(),
507
661
  fbp: getFbpCookie(),
@@ -531,14 +685,12 @@ function markFired(input) {
531
685
  } catch {
532
686
  }
533
687
  }
534
- function fireOnce(input) {
688
+ function fireConversionWithConsent(input, options) {
689
+ if (getConsentState() === "denied") return;
535
690
  if (alreadyFired(input)) return;
691
+ applyUserDataForConversion(options?.userData);
536
692
  if (fireGtagConversion(input)) markFired(input);
537
693
  }
538
- function fireConversionWithConsent(input) {
539
- if (getConsentState() === "denied") return;
540
- fireOnce(input);
541
- }
542
694
 
543
695
  // ../tracking-core/src/resources/conversion-config.ts
544
696
  function isStringMap(value) {
@@ -983,6 +1135,7 @@ var TrackingConfigRuntime = class {
983
1135
  if (!firing || getConsentState() === "denied") continue;
984
1136
  const currency = item.currency ?? firing.currency ?? null;
985
1137
  const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
1138
+ applyUserDataForConversion();
986
1139
  fireGtagConversion({
987
1140
  sendTo: firing.send_to,
988
1141
  value,
@@ -1514,6 +1667,8 @@ function createTrackingClient(config) {
1514
1667
  session_id: sessionId,
1515
1668
  visitor_id: visitorId,
1516
1669
  gclid: params.gclid,
1670
+ wbraid: params.wbraid,
1671
+ gbraid: params.gbraid,
1517
1672
  fbclid: params.fbclid,
1518
1673
  fbc: getFbcCookie(),
1519
1674
  fbp: getFbpCookie(),
@@ -1560,6 +1715,13 @@ function createTrackingClient(config) {
1560
1715
  function trackEvent(input) {
1561
1716
  if (destroyed) return;
1562
1717
  if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
1718
+ if (input.eventType === "form_submit" && getConsentState() !== "denied") {
1719
+ try {
1720
+ const fields = input.metadata?.form?.fields;
1721
+ if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);
1722
+ } catch {
1723
+ }
1724
+ }
1563
1725
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
1564
1726
  queue.push({
1565
1727
  event_type: input.eventType,
@@ -2523,46 +2685,6 @@ function attachCtaClickCapture(client, config) {
2523
2685
  };
2524
2686
  }
2525
2687
 
2526
- // ../tracking-core/src/phone.ts
2527
- import { AsYouType, parsePhoneNumberFromString } from "libphonenumber-js";
2528
- var DEFAULT_PHONE_COUNTRY = "CA";
2529
- function parsePhone(raw, country) {
2530
- const region = country ?? DEFAULT_PHONE_COUNTRY;
2531
- const parsed = parsePhoneNumberFromString(raw ?? "", region);
2532
- if (!parsed) {
2533
- return { e164: null, national: "", international: "", country: region, isValid: false };
2534
- }
2535
- const isValid = parsed.isValid();
2536
- return {
2537
- // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
2538
- // (e.g. too few digits) still parses but must not be transmitted.
2539
- e164: isValid ? parsed.number : null,
2540
- national: parsed.formatNational(),
2541
- international: parsed.formatInternational(),
2542
- country: parsed.country ?? region,
2543
- isValid
2544
- };
2545
- }
2546
- function toE164(raw, country) {
2547
- return parsePhone(raw, country).e164;
2548
- }
2549
- function formatPhone(value, format = "national", country) {
2550
- const parsed = parsePhone(value, country);
2551
- if (typeof format === "function") return format(parsed);
2552
- switch (format) {
2553
- case "international":
2554
- return parsed.international || value;
2555
- case "e164":
2556
- return parsed.e164 ?? value;
2557
- case "national":
2558
- default:
2559
- return parsed.national || value;
2560
- }
2561
- }
2562
- function formatPhoneAsTyped(raw, country) {
2563
- return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
2564
- }
2565
-
2566
2688
  // ../tracking-core/src/triggers/phone-click-capture.ts
2567
2689
  var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2568
2690
  function safeDecodeURIComponent(value) {
@@ -2672,6 +2794,8 @@ async function salesRequest(config, method, path, body) {
2672
2794
  // ../tracking-core/src/resources/sales/client.ts
2673
2795
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
2674
2796
  if (!firing) return;
2797
+ const userData = input.customer_email || input.customer_phone ? { email: input.customer_email ?? null, phone: input.customer_phone ?? null } : null;
2798
+ if (userData && getConsentState() !== "denied") stashUserData(userData);
2675
2799
  const txnBase = input.external_id ?? sale.id;
2676
2800
  for (const item of recorded) {
2677
2801
  if (!item.service) continue;
@@ -2686,12 +2810,15 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2686
2810
  const config = firing.getFiring(item.service);
2687
2811
  if (!config) continue;
2688
2812
  const cents = item.amount_cents ?? config.value_cents ?? null;
2689
- fireConversionWithConsent({
2690
- sendTo: config.send_to,
2691
- value: cents != null ? fromMinor(cents, currency) : null,
2692
- currency: config.currency ?? currency,
2693
- transactionId: `${txnBase}:${item.service}`
2694
- });
2813
+ fireConversionWithConsent(
2814
+ {
2815
+ sendTo: config.send_to,
2816
+ value: cents != null ? fromMinor(cents, currency) : null,
2817
+ currency: config.currency ?? currency,
2818
+ transactionId: `${txnBase}:${item.service}`
2819
+ },
2820
+ { userData }
2821
+ );
2695
2822
  }
2696
2823
  }
2697
2824
  function createSalesClient(config) {
@@ -2726,6 +2853,7 @@ function createSalesClient(config) {
2726
2853
  recordSale: record,
2727
2854
  trackConversion(key, options) {
2728
2855
  if (config.firing && "fireConversion" in config.firing) {
2856
+ if (options?.userData && getConsentState() !== "denied") stashUserData(options.userData);
2729
2857
  config.firing.fireConversion(key, {
2730
2858
  value: options?.value ?? void 0,
2731
2859
  currency: options?.currency ?? config.defaultCurrency ?? void 0,
@@ -2738,12 +2866,15 @@ function createSalesClient(config) {
2738
2866
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
2739
2867
  const cents = firing.value_cents ?? null;
2740
2868
  const value = options?.value ?? (cents != null && currency ? fromMinor(cents, currency) : null);
2741
- fireConversionWithConsent({
2742
- sendTo: firing.send_to,
2743
- value,
2744
- currency,
2745
- transactionId: options?.transactionId ?? null
2746
- });
2869
+ fireConversionWithConsent(
2870
+ {
2871
+ sendTo: firing.send_to,
2872
+ value,
2873
+ currency,
2874
+ transactionId: options?.transactionId ?? null
2875
+ },
2876
+ { userData: options?.userData }
2877
+ );
2747
2878
  },
2748
2879
  async list(query) {
2749
2880
  const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};
@@ -3288,7 +3419,7 @@ function GoogleAdsTracking(props) {
3288
3419
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
3289
3420
 
3290
3421
  // package.json
3291
- var version = "0.18.0";
3422
+ var version = "0.19.0";
3292
3423
 
3293
3424
  // ../tracking-core/src/phone-react.tsx
3294
3425
  import {
@@ -3558,6 +3689,7 @@ export {
3558
3689
  TRACKING_PARAM_KEYS,
3559
3690
  TRACKING_RANGES,
3560
3691
  captureTrackingParamsFromLocation,
3692
+ clearStashedUserData,
3561
3693
  createSalesClient,
3562
3694
  createTracking,
3563
3695
  createTrackingClientContext,
@@ -3586,6 +3718,7 @@ export {
3586
3718
  saleUpdateSchema,
3587
3719
  salesRequest,
3588
3720
  setConsentState,
3721
+ stashUserData,
3589
3722
  toE164,
3590
3723
  toMinor,
3591
3724
  useConsent,