@aranova/tracking-react 0.11.0 → 0.12.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/index.js CHANGED
@@ -22,8 +22,13 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  AranovaApiError: () => AranovaApiError,
24
24
  ConsentBanner: () => ConsentBanner,
25
+ DEFAULT_PHONE_COUNTRY: () => DEFAULT_PHONE_COUNTRY,
25
26
  GoogleAdsTracking: () => GoogleAdsTracking,
27
+ NAMED_RANGES: () => NAMED_RANGES,
28
+ PhoneField: () => PhoneField,
29
+ SUPPORTED_CURRENCIES: () => SUPPORTED_CURRENCIES,
26
30
  TRACKING_PARAM_KEYS: () => TRACKING_PARAM_KEYS,
31
+ TRACKING_RANGES: () => TRACKING_RANGES,
27
32
  captureTrackingParamsFromLocation: () => captureTrackingParamsFromLocation,
28
33
  createSalesClient: () => createSalesClient,
29
34
  createTracking: () => createTracking,
@@ -31,15 +36,28 @@ __export(src_exports, {
31
36
  createTrackingEventCreatePayload: () => createTrackingEventCreatePayload,
32
37
  createTrackingSessionUpsertPayload: () => createTrackingSessionUpsertPayload,
33
38
  fetchServices: () => fetchServices,
39
+ formatDateInTz: () => formatDateInTz,
34
40
  formatMoney: () => formatMoney,
41
+ formatPhone: () => formatPhone,
42
+ formatPhoneAsTyped: () => formatPhoneAsTyped,
35
43
  fromMinor: () => fromMinor,
36
44
  getConsentState: () => getConsentState,
45
+ parsePhone: () => parsePhone,
46
+ phoneField: () => phoneField,
37
47
  resetConsent: () => resetConsent,
48
+ saleCreateSchema: () => saleCreateSchema,
49
+ saleItemSchema: () => saleItemSchema,
50
+ saleServiceSchema: () => saleServiceSchema,
51
+ saleUpdateSchema: () => saleUpdateSchema,
52
+ salesRequest: () => salesRequest,
38
53
  setConsentState: () => setConsentState,
54
+ toE164: () => toE164,
39
55
  toMinor: () => toMinor,
40
56
  useConsent: () => useConsent,
41
57
  useConsentState: () => useConsentState,
42
58
  useGclid: () => useGclid,
59
+ usePhoneConfig: () => usePhoneConfig,
60
+ usePhoneField: () => usePhoneField,
43
61
  useTrackingParams: () => useTrackingParams
44
62
  });
45
63
  module.exports = __toCommonJS(src_exports);
@@ -62,31 +80,26 @@ function buildConsentPayload(state) {
62
80
  };
63
81
  }
64
82
  function getConsentState() {
65
- if (typeof window === "undefined")
66
- return "pending";
83
+ if (typeof window === "undefined") return "pending";
67
84
  const storedState = window.localStorage.getItem(CONSENT_STATE_KEY);
68
- if (storedState === "granted" || storedState === "denied")
69
- return storedState;
85
+ if (storedState === "granted" || storedState === "denied") return storedState;
70
86
  return "pending";
71
87
  }
72
88
  function setConsentState(state) {
73
- if (typeof window === "undefined")
74
- return;
89
+ if (typeof window === "undefined") return;
75
90
  window.localStorage.setItem(CONSENT_STATE_KEY, state);
76
91
  window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, (/* @__PURE__ */ new Date()).toISOString());
77
92
  if (typeof window.gtag === "function")
78
93
  window.gtag("consent", "update", buildConsentPayload(state));
79
94
  }
80
95
  function resetConsent() {
81
- if (typeof window === "undefined")
82
- return;
96
+ if (typeof window === "undefined") return;
83
97
  window.localStorage.removeItem(CONSENT_STATE_KEY);
84
98
  window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);
85
99
  }
86
100
  function restoreStoredConsent() {
87
101
  const consentState = getConsentState();
88
- if (consentState === "granted" || consentState === "denied")
89
- setConsentState(consentState);
102
+ if (consentState === "granted" || consentState === "denied") setConsentState(consentState);
90
103
  return consentState;
91
104
  }
92
105
 
@@ -143,8 +156,7 @@ function isValidGtagId(id) {
143
156
  }
144
157
  function ensureGtagFunction() {
145
158
  window.dataLayer = window.dataLayer || [];
146
- if (typeof window.gtag === "function")
147
- return window.gtag;
159
+ if (typeof window.gtag === "function") return window.gtag;
148
160
  window.gtag = (...args) => {
149
161
  window.dataLayer?.push(args);
150
162
  };
@@ -161,17 +173,17 @@ function applyDefaultConsentState() {
161
173
  });
162
174
  }
163
175
  function loadGtagScript(gtagId) {
164
- if (typeof document === "undefined")
165
- return;
176
+ if (typeof document === "undefined") return;
166
177
  const marker = getScriptMarker("gtag-loader");
167
- const existingScript = document.querySelector(`script[${TRACKING_SCRIPT_ATTRIBUTE}="${marker}"]`);
168
- if (existingScript)
169
- return;
178
+ const existingScript = document.querySelector(
179
+ `script[${TRACKING_SCRIPT_ATTRIBUTE}="${marker}"]`
180
+ );
181
+ if (existingScript) return;
170
182
  const script = document.createElement("script");
171
183
  script.async = true;
172
184
  script.src = `${GTAG_SCRIPT_HOST}?id=${encodeURIComponent(gtagId)}`;
173
185
  script.setAttribute(TRACKING_SCRIPT_ATTRIBUTE, marker);
174
- document.head.appendChild(script);
186
+ document.head.append(script);
175
187
  }
176
188
  function initializeGtag(gtagId) {
177
189
  const gtag = ensureGtagFunction();
@@ -179,21 +191,17 @@ function initializeGtag(gtagId) {
179
191
  gtag("config", gtagId);
180
192
  }
181
193
  function bootstrapGoogleAdsTracking(gtagId) {
182
- if (typeof window === "undefined" || typeof document === "undefined")
183
- return;
184
- if (!isValidGtagId(gtagId))
185
- return;
194
+ if (typeof window === "undefined" || typeof document === "undefined") return;
195
+ if (!isValidGtagId(gtagId)) return;
186
196
  applyDefaultConsentState();
187
197
  loadGtagScript(gtagId);
188
198
  initializeGtag(gtagId);
189
199
  restoreStoredConsent();
190
200
  }
191
201
  function bootstrapMultipleGtags(gtagIds) {
192
- if (typeof window === "undefined" || typeof document === "undefined")
193
- return;
202
+ if (typeof window === "undefined" || typeof document === "undefined") return;
194
203
  const ids = Object.values(gtagIds).filter(isValidGtagId);
195
- if (ids.length === 0)
196
- return;
204
+ if (ids.length === 0) return;
197
205
  applyDefaultConsentState();
198
206
  loadGtagScript(ids[0]);
199
207
  const gtag = ensureGtagFunction();
@@ -245,18 +253,15 @@ function getTrackingQueryValues(searchParams) {
245
253
  }, {});
246
254
  }
247
255
  function getCookieValueFromDocument(key) {
248
- if (typeof document === "undefined")
249
- return null;
256
+ if (typeof document === "undefined") return null;
250
257
  const cookies = document.cookie ? document.cookie.split("; ") : [];
251
258
  const match = cookies.find((cookie) => cookie.startsWith(`${key}=`));
252
- if (!match)
253
- return null;
259
+ if (!match) return null;
254
260
  const [, rawValue = ""] = match.split("=");
255
261
  return normalizeTrackingCookieValue(decodeURIComponent(rawValue));
256
262
  }
257
263
  function setTrackingCookie(key, value, maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS) {
258
- if (typeof document === "undefined")
259
- return;
264
+ if (typeof document === "undefined") return;
260
265
  const encodedValue = encodeURIComponent(value);
261
266
  document.cookie = `${key}=${encodedValue}; Max-Age=${maxAgeSeconds}; Path=/; SameSite=Lax`;
262
267
  }
@@ -296,18 +301,15 @@ function writeLocalStorage(key, value) {
296
301
  }
297
302
  }
298
303
  function getVisitorId() {
299
- if (typeof window === "undefined")
300
- return safeUuid();
304
+ if (typeof window === "undefined") return safeUuid();
301
305
  const existing = readLocalStorage(VISITOR_STORAGE_KEY);
302
- if (existing && existing.length > 0)
303
- return existing;
306
+ if (existing && existing.length > 0) return existing;
304
307
  const fresh = safeUuid();
305
308
  writeLocalStorage(VISITOR_STORAGE_KEY, fresh);
306
309
  return fresh;
307
310
  }
308
311
  function getOrRotateSessionId(now = Date.now()) {
309
- if (typeof window === "undefined")
310
- return { id: safeUuid(), isNew: true };
312
+ if (typeof window === "undefined") return { id: safeUuid(), isNew: true };
311
313
  const raw = readLocalStorage(SESSION_STORAGE_KEY);
312
314
  if (raw) {
313
315
  try {
@@ -380,8 +382,7 @@ function setLastFiredUrl(url) {
380
382
  writeSessionStorage(LAST_FIRED_URL_STORAGE_KEY, url);
381
383
  }
382
384
  function buildPageViewMetadata(referrerOverride) {
383
- if (typeof window === "undefined" || typeof document === "undefined")
384
- return null;
385
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
385
386
  return pageViewMetadataSchema.parse({
386
387
  page: {
387
388
  title: document.title || null,
@@ -394,8 +395,7 @@ function buildPageViewMetadata(referrerOverride) {
394
395
  });
395
396
  }
396
397
  function fireManualPageView(client) {
397
- if (typeof window === "undefined")
398
- return;
398
+ if (typeof window === "undefined") return;
399
399
  const currentHref = window.location.href;
400
400
  const previousFiredUrl = getLastFiredUrl();
401
401
  const internalReferrer = previousFiredUrl !== null && previousFiredUrl !== currentHref ? previousFiredUrl : null;
@@ -431,8 +431,7 @@ function attachAutoPageView(client, options = {}) {
431
431
  let lastPath = window.location.pathname + window.location.search;
432
432
  function maybeFire() {
433
433
  const current = window.location.pathname + window.location.search;
434
- if (current === lastPath)
435
- return;
434
+ if (current === lastPath) return;
436
435
  lastPath = current;
437
436
  fireManualPageView(client);
438
437
  }
@@ -454,8 +453,7 @@ function attachAutoPageView(client, options = {}) {
454
453
  history.replaceState = patchedReplaceState;
455
454
  window.addEventListener("popstate", maybeFire);
456
455
  window.addEventListener("pageshow", handlePageShow);
457
- if (!options.skipInitial)
458
- fireManualPageView(client);
456
+ if (!options.skipInitial) fireManualPageView(client);
459
457
  return () => {
460
458
  history.pushState = originalPushState;
461
459
  history.replaceState = originalReplaceState;
@@ -537,8 +535,7 @@ function buildContext(surface, sdkVersion, packageName, environment, activeGtagI
537
535
  };
538
536
  }
539
537
  function readTrackingParams() {
540
- if (typeof window === "undefined")
541
- return createEmptyTrackingParams();
538
+ if (typeof window === "undefined") return createEmptyTrackingParams();
542
539
  try {
543
540
  captureTrackingParamsFromLocation();
544
541
  } catch {
@@ -553,8 +550,7 @@ function consentSnapshot() {
553
550
  }
554
551
  }
555
552
  async function postWithFetch(url, body, apiKey, identity, keepalive) {
556
- if (typeof fetch !== "function")
557
- return;
553
+ if (typeof fetch !== "function") return;
558
554
  try {
559
555
  await fetch(url, {
560
556
  method: "POST",
@@ -614,8 +610,7 @@ function createTrackingClient(config) {
614
610
  const visitorId = getVisitorId();
615
611
  const initialSession = getOrRotateSessionId();
616
612
  let sessionId = initialSession.id;
617
- if (typeof window !== "undefined")
618
- firstPage = window.location.href;
613
+ if (typeof window !== "undefined") firstPage = window.location.href;
619
614
  function enqueueHeartbeat() {
620
615
  const metadata = buildHeartbeatMetadata(
621
616
  config.surface,
@@ -641,7 +636,13 @@ function createTrackingClient(config) {
641
636
  }
642
637
  sessionId = rotated.id;
643
638
  const params = readTrackingParams();
644
- const context = buildContext(config.surface, sdkVersion, packageName, environment, activeGtagIds);
639
+ const context = buildContext(
640
+ config.surface,
641
+ sdkVersion,
642
+ packageName,
643
+ environment,
644
+ activeGtagIds
645
+ );
645
646
  return {
646
647
  session_id: sessionId,
647
648
  visitor_id: visitorId,
@@ -658,8 +659,7 @@ function createTrackingClient(config) {
658
659
  };
659
660
  }
660
661
  function scheduleFlush() {
661
- if (flushTimer !== null || destroyed)
662
- return;
662
+ if (flushTimer !== null || destroyed) return;
663
663
  flushTimer = setTimeout(() => {
664
664
  flushTimer = null;
665
665
  void flush();
@@ -672,8 +672,7 @@ function createTrackingClient(config) {
672
672
  }
673
673
  }
674
674
  async function flush() {
675
- if (queue.length === 0)
676
- return;
675
+ if (queue.length === 0) return;
677
676
  const events = queue.slice(0, HARD_MAX_BATCH);
678
677
  queue = queue.slice(events.length);
679
678
  clearScheduledFlush();
@@ -685,10 +684,8 @@ function createTrackingClient(config) {
685
684
  await postWithFetch(eventsUrl, serialized, config.apiKey, identityHeaders2, false);
686
685
  }
687
686
  function trackEvent(input) {
688
- if (destroyed)
689
- return;
690
- if (!input || typeof input.eventType !== "string" || input.eventType.length === 0)
691
- return;
687
+ if (destroyed) return;
688
+ if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
692
689
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
693
690
  queue.push({
694
691
  event_type: input.eventType,
@@ -703,8 +700,7 @@ function createTrackingClient(config) {
703
700
  }
704
701
  }
705
702
  function flushOnUnload() {
706
- if (queue.length === 0)
707
- return;
703
+ if (queue.length === 0) return;
708
704
  const events = queue.slice(0, HARD_MAX_BATCH);
709
705
  queue = queue.slice(events.length);
710
706
  clearScheduledFlush();
@@ -1379,21 +1375,40 @@ function createSalesClient(config) {
1379
1375
  return salesRequest(config, "POST", "/sales", body);
1380
1376
  },
1381
1377
  async list(query) {
1382
- const { cursor, limit, ...filters } = query ?? {};
1378
+ const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};
1383
1379
  return salesRequest(config, "POST", "/sales/query", {
1384
1380
  filters,
1385
1381
  ...limit !== void 0 ? { limit } : {},
1386
- ...cursor !== void 0 ? { cursor } : {}
1382
+ ...cursor !== void 0 ? { cursor } : {},
1383
+ ...sort !== void 0 ? { sort } : {},
1384
+ ...order !== void 0 ? { order } : {},
1385
+ ...want_total !== void 0 ? { want_total } : {}
1387
1386
  });
1388
1387
  },
1389
1388
  async summary(query) {
1390
- const { range, include_categories, include_deleted_services, top_n, ...filters } = query;
1389
+ const {
1390
+ range,
1391
+ include_categories,
1392
+ include_deleted_services,
1393
+ top_n,
1394
+ since,
1395
+ until,
1396
+ timezone,
1397
+ granularity,
1398
+ compare_to,
1399
+ ...filters
1400
+ } = query;
1391
1401
  return salesRequest(config, "POST", "/sales/summary", {
1392
1402
  filters,
1393
- range,
1403
+ ...range !== void 0 ? { range } : {},
1394
1404
  ...include_categories !== void 0 ? { include_categories } : {},
1395
1405
  ...include_deleted_services !== void 0 ? { include_deleted_services } : {},
1396
- ...top_n !== void 0 ? { top_n } : {}
1406
+ ...top_n !== void 0 ? { top_n } : {},
1407
+ ...since !== void 0 ? { since } : {},
1408
+ ...until !== void 0 ? { until } : {},
1409
+ ...timezone !== void 0 ? { timezone } : {},
1410
+ ...granularity !== void 0 ? { granularity } : {},
1411
+ ...compare_to !== void 0 ? { compare_to } : {}
1397
1412
  });
1398
1413
  },
1399
1414
  async get(id) {
@@ -1404,15 +1419,53 @@ function createSalesClient(config) {
1404
1419
  },
1405
1420
  async delete(id) {
1406
1421
  await salesRequest(config, "DELETE", `/sales/${id}`);
1422
+ },
1423
+ customers: {
1424
+ async list(query) {
1425
+ const { segment, sort, order, cursor, limit, want_total, ...filters } = query ?? {};
1426
+ return salesRequest(config, "POST", "/customers/query", {
1427
+ filters,
1428
+ ...segment !== void 0 ? { segment } : {},
1429
+ ...sort !== void 0 ? { sort } : {},
1430
+ ...order !== void 0 ? { order } : {},
1431
+ ...cursor !== void 0 ? { cursor } : {},
1432
+ ...limit !== void 0 ? { limit } : {},
1433
+ ...want_total !== void 0 ? { want_total } : {}
1434
+ });
1435
+ },
1436
+ async get(id, options) {
1437
+ const params = new URLSearchParams();
1438
+ if (options?.include_sales !== void 0)
1439
+ params.set("include_sales", String(options.include_sales));
1440
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
1441
+ if (options?.cursor != null) params.set("cursor", options.cursor);
1442
+ const qs = params.toString();
1443
+ return salesRequest(
1444
+ config,
1445
+ "GET",
1446
+ `/customers/${encodeURIComponent(id)}${qs ? `?${qs}` : ""}`
1447
+ );
1448
+ },
1449
+ async summary(query) {
1450
+ const { range, since, until, timezone, compare_to, ...filters } = query ?? {};
1451
+ return salesRequest(config, "POST", "/customers/summary", {
1452
+ filters,
1453
+ ...range !== void 0 ? { range } : {},
1454
+ ...since !== void 0 ? { since } : {},
1455
+ ...until !== void 0 ? { until } : {},
1456
+ ...timezone !== void 0 ? { timezone } : {},
1457
+ ...compare_to !== void 0 ? { compare_to } : {}
1458
+ });
1459
+ }
1460
+ },
1461
+ business: {
1462
+ async config() {
1463
+ return salesRequest(config, "GET", "/business/config");
1464
+ }
1407
1465
  }
1408
1466
  };
1409
1467
  }
1410
1468
 
1411
- // ../tracking-core/src/resources/services.ts
1412
- async function fetchServices(config) {
1413
- return salesRequest(config, "GET", "/services");
1414
- }
1415
-
1416
1469
  // ../tracking-core/src/resources/sales/money.ts
1417
1470
  var MINOR_UNIT_EXPONENT = {
1418
1471
  USD: 2,
@@ -1432,6 +1485,181 @@ function formatMoney(cents, currency, locale) {
1432
1485
  fromMinor(cents, currency)
1433
1486
  );
1434
1487
  }
1488
+ function formatDateInTz(iso, timeZone, opts, locale) {
1489
+ const date = new Date(iso);
1490
+ if (Number.isNaN(date.getTime())) return iso;
1491
+ return new Intl.DateTimeFormat(locale, {
1492
+ year: "numeric",
1493
+ month: "short",
1494
+ day: "2-digit",
1495
+ hour: "2-digit",
1496
+ minute: "2-digit",
1497
+ ...opts,
1498
+ timeZone
1499
+ }).format(date);
1500
+ }
1501
+
1502
+ // ../tracking-core/src/resources/sales/schema.ts
1503
+ var import_zod11 = require("zod");
1504
+ var SUPPORTED_CURRENCIES = ["USD", "CAD"];
1505
+ var TRACKING_ENVIRONMENTS = ["production", "development"];
1506
+ var currencySchema = import_zod11.z.enum(SUPPORTED_CURRENCIES);
1507
+ var centsSchema = import_zod11.z.number().int().nonnegative();
1508
+ var quantitySchema = import_zod11.z.string().regex(/^\d+(\.\d{1,3})?$/);
1509
+ var metadataSchema = import_zod11.z.record(import_zod11.z.unknown());
1510
+ var saleItemSchema = import_zod11.z.object({
1511
+ external_item_id: import_zod11.z.string().nullable().optional(),
1512
+ name: import_zod11.z.string().nullable().optional(),
1513
+ category: import_zod11.z.string().nullable().optional(),
1514
+ quantity: quantitySchema,
1515
+ unit_price_cents: centsSchema,
1516
+ // Non-negativity validated on the wire — same contract as the other cents
1517
+ // fields — and backstopped by the DB CHECK.
1518
+ unit_cost_cents: centsSchema.nullable().optional()
1519
+ }).strict();
1520
+ var saleServiceSchema = import_zod11.z.object({
1521
+ service: import_zod11.z.string(),
1522
+ amount_cents: centsSchema
1523
+ }).strict();
1524
+ var customerNameSchema = import_zod11.z.string().max(200);
1525
+ var customerPhoneSchema = import_zod11.z.string().max(64);
1526
+ var customerEmailSchema = import_zod11.z.string().max(320).email();
1527
+ function refineServiceXor(val, ctx, { requireAmount }) {
1528
+ if (val.services != null) {
1529
+ if (val.service != null) {
1530
+ ctx.addIssue({
1531
+ code: import_zod11.z.ZodIssueCode.custom,
1532
+ message: "pass either `service` or `services`, not both",
1533
+ path: ["services"]
1534
+ });
1535
+ }
1536
+ if (val.services.length === 0) {
1537
+ ctx.addIssue({
1538
+ code: import_zod11.z.ZodIssueCode.custom,
1539
+ message: "`services` must not be empty",
1540
+ path: ["services"]
1541
+ });
1542
+ }
1543
+ const keys = val.services.map((s) => s.service);
1544
+ if (new Set(keys).size !== keys.length) {
1545
+ ctx.addIssue({
1546
+ code: import_zod11.z.ZodIssueCode.custom,
1547
+ message: "`services` must not list the same service more than once",
1548
+ path: ["services"]
1549
+ });
1550
+ }
1551
+ if (val.amount_total_cents != null) {
1552
+ const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);
1553
+ if (val.amount_total_cents !== sum) {
1554
+ ctx.addIssue({
1555
+ code: import_zod11.z.ZodIssueCode.custom,
1556
+ message: "amount_total_cents must equal the sum of the services amounts (omit it to derive it automatically)",
1557
+ path: ["amount_total_cents"]
1558
+ });
1559
+ }
1560
+ }
1561
+ } else if (requireAmount && val.amount_total_cents == null) {
1562
+ ctx.addIssue({
1563
+ code: import_zod11.z.ZodIssueCode.custom,
1564
+ message: "amount_total_cents is required unless `services` is provided",
1565
+ path: ["amount_total_cents"]
1566
+ });
1567
+ }
1568
+ }
1569
+ var saleCreateSchema = import_zod11.z.object({
1570
+ external_id: import_zod11.z.string().nullable().optional(),
1571
+ description: import_zod11.z.string().nullable().optional(),
1572
+ service: import_zod11.z.string().nullable().optional(),
1573
+ services: import_zod11.z.array(saleServiceSchema).nullable().optional(),
1574
+ currency: currencySchema,
1575
+ // Optional only because the plural `services` form derives it from the sum
1576
+ // (see refineServiceXor); the singular/serviceless path still requires it.
1577
+ amount_total_cents: centsSchema.nullable().optional(),
1578
+ occurred_at: import_zod11.z.string().datetime(),
1579
+ environment: import_zod11.z.enum(TRACKING_ENVIRONMENTS).default("production"),
1580
+ items: import_zod11.z.array(saleItemSchema).default([]),
1581
+ metadata: metadataSchema.nullable().optional(),
1582
+ customer_name: customerNameSchema.nullable().optional(),
1583
+ customer_phone: customerPhoneSchema.nullable().optional(),
1584
+ customer_email: customerEmailSchema.nullable().optional()
1585
+ }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));
1586
+ var saleUpdateSchema = import_zod11.z.object({
1587
+ description: import_zod11.z.string().nullable().optional(),
1588
+ service: import_zod11.z.string().nullable().optional(),
1589
+ services: import_zod11.z.array(saleServiceSchema).nullable().optional(),
1590
+ currency: currencySchema.optional(),
1591
+ amount_total_cents: centsSchema.optional(),
1592
+ occurred_at: import_zod11.z.string().datetime().optional(),
1593
+ items: import_zod11.z.array(saleItemSchema).optional(),
1594
+ metadata: metadataSchema.nullable().optional(),
1595
+ customer_name: customerNameSchema.nullable().optional(),
1596
+ customer_phone: customerPhoneSchema.nullable().optional(),
1597
+ customer_email: customerEmailSchema.nullable().optional()
1598
+ }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: false }));
1599
+ var TRACKING_RANGES = ["24h", "7d", "30d"];
1600
+ var NAMED_RANGES = [
1601
+ "today",
1602
+ "yesterday",
1603
+ "wtd",
1604
+ "mtd",
1605
+ "qtd",
1606
+ "ytd",
1607
+ "24h",
1608
+ "7d",
1609
+ "30d",
1610
+ "90d",
1611
+ "custom"
1612
+ ];
1613
+
1614
+ // ../tracking-core/src/resources/services.ts
1615
+ async function fetchServices(config) {
1616
+ return salesRequest(config, "GET", "/services");
1617
+ }
1618
+
1619
+ // ../tracking-core/src/phone.ts
1620
+ var import_libphonenumber_js = require("libphonenumber-js");
1621
+ var DEFAULT_PHONE_COUNTRY = "CA";
1622
+ function parsePhone(raw, country) {
1623
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
1624
+ const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
1625
+ if (!parsed) {
1626
+ return { e164: null, national: "", international: "", country: region, isValid: false };
1627
+ }
1628
+ const isValid = parsed.isValid();
1629
+ return {
1630
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
1631
+ // (e.g. too few digits) still parses but must not be transmitted.
1632
+ e164: isValid ? parsed.number : null,
1633
+ national: parsed.formatNational(),
1634
+ international: parsed.formatInternational(),
1635
+ country: parsed.country ?? region,
1636
+ isValid
1637
+ };
1638
+ }
1639
+ function toE164(raw, country) {
1640
+ return parsePhone(raw, country).e164;
1641
+ }
1642
+ function formatPhone(value, format = "national", country) {
1643
+ const parsed = parsePhone(value, country);
1644
+ if (typeof format === "function") return format(parsed);
1645
+ switch (format) {
1646
+ case "international":
1647
+ return parsed.international || value;
1648
+ case "e164":
1649
+ return parsed.e164 ?? value;
1650
+ case "national":
1651
+ default:
1652
+ return parsed.national || value;
1653
+ }
1654
+ }
1655
+ function formatPhoneAsTyped(raw, country) {
1656
+ return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
1657
+ }
1658
+
1659
+ // ../tracking-core/src/phone-field.ts
1660
+ function phoneField(name, raw, country) {
1661
+ return { name, type: "phone", value: toE164(raw, country) };
1662
+ }
1435
1663
 
1436
1664
  // src/hooks.ts
1437
1665
  function useGclid() {
@@ -1673,10 +1901,7 @@ var ANIMATION_KEYFRAMES = `
1673
1901
  var import_react3 = require("react");
1674
1902
  function GoogleAdsTracking(props) {
1675
1903
  const { gtagId, gtagIds } = props;
1676
- const gtagIdsKey = (0, import_react3.useMemo)(
1677
- () => gtagIds ? JSON.stringify(gtagIds) : "",
1678
- [gtagIds]
1679
- );
1904
+ const gtagIdsKey = (0, import_react3.useMemo)(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
1680
1905
  (0, import_react3.useEffect)(() => {
1681
1906
  if (gtagIds && Object.keys(gtagIds).length > 0) {
1682
1907
  bootstrapMultipleGtags(gtagIds);
@@ -1688,13 +1913,99 @@ function GoogleAdsTracking(props) {
1688
1913
  }
1689
1914
 
1690
1915
  // src/factory.tsx
1691
- var import_react4 = require("react");
1916
+ var import_react5 = require("react");
1692
1917
 
1693
1918
  // package.json
1694
- var version = "0.11.0";
1919
+ var version = "0.12.1";
1695
1920
 
1696
- // src/factory.tsx
1921
+ // ../tracking-core/src/phone-react.tsx
1922
+ var import_react4 = require("react");
1697
1923
  var import_jsx_runtime2 = require("react/jsx-runtime");
1924
+ var _phoneConfigContext;
1925
+ function phoneConfigContext() {
1926
+ return _phoneConfigContext ?? (_phoneConfigContext = (0, import_react4.createContext)(null));
1927
+ }
1928
+ function PhoneConfigProvider({
1929
+ value,
1930
+ children
1931
+ }) {
1932
+ const Ctx = phoneConfigContext();
1933
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Ctx.Provider, { value, children });
1934
+ }
1935
+ function usePhoneConfig() {
1936
+ const ctx = (0, import_react4.useContext)(phoneConfigContext());
1937
+ return {
1938
+ defaultCountry: ctx?.defaultCountry ?? DEFAULT_PHONE_COUNTRY,
1939
+ display: ctx?.display ?? "national"
1940
+ };
1941
+ }
1942
+ function usePhoneField(opts = {}) {
1943
+ const cfg = usePhoneConfig();
1944
+ const country = opts.country ?? cfg.defaultCountry;
1945
+ const display = opts.display ?? cfg.display;
1946
+ const { onValueChange } = opts;
1947
+ const [value, setValue] = (0, import_react4.useState)(() => formatPhoneAsTyped(opts.defaultValue ?? "", country));
1948
+ const [touched, setTouched] = (0, import_react4.useState)(false);
1949
+ const parsed = (0, import_react4.useMemo)(() => parsePhone(value, country), [value, country]);
1950
+ const onChange = (0, import_react4.useCallback)(
1951
+ (event) => {
1952
+ const next = formatPhoneAsTyped(event.target.value, country);
1953
+ setValue(next);
1954
+ onValueChange?.(parsePhone(next, country).e164);
1955
+ },
1956
+ [country, onValueChange]
1957
+ );
1958
+ const onBlur = (0, import_react4.useCallback)(
1959
+ (_event) => {
1960
+ setTouched(true);
1961
+ setValue((current) => {
1962
+ const p = parsePhone(current, country);
1963
+ return p.isValid ? formatPhone(current, display, country) : current;
1964
+ });
1965
+ },
1966
+ [country, display]
1967
+ );
1968
+ const error = touched && value.length > 0 && !parsed.isValid ? "Enter a valid phone number" : null;
1969
+ return {
1970
+ value,
1971
+ e164: parsed.e164,
1972
+ isValid: parsed.isValid,
1973
+ error,
1974
+ parsed,
1975
+ inputProps: { value, onChange, onBlur, type: "tel", inputMode: "tel", autoComplete: "tel" }
1976
+ };
1977
+ }
1978
+ var PhoneField = (0, import_react4.forwardRef)(function PhoneField2({ country, value, defaultValue, onChange, onE164Change, ...rest }, ref) {
1979
+ const cfg = usePhoneConfig();
1980
+ const resolvedCountry = country ?? cfg.defaultCountry;
1981
+ const isControlled = value !== void 0;
1982
+ const [internal, setInternal] = (0, import_react4.useState)(
1983
+ () => formatPhoneAsTyped(defaultValue ?? "", resolvedCountry)
1984
+ );
1985
+ const handleChange = (event) => {
1986
+ const formatted = formatPhoneAsTyped(event.target.value, resolvedCountry);
1987
+ event.target.value = formatted;
1988
+ onE164Change?.(parsePhone(formatted, resolvedCountry).e164);
1989
+ if (!isControlled) setInternal(formatted);
1990
+ onChange?.(event);
1991
+ };
1992
+ const shown = isControlled ? formatPhoneAsTyped(value, resolvedCountry) : internal;
1993
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1994
+ "input",
1995
+ {
1996
+ ...rest,
1997
+ ref,
1998
+ type: "tel",
1999
+ inputMode: "tel",
2000
+ autoComplete: "tel",
2001
+ value: shown,
2002
+ onChange: handleChange
2003
+ }
2004
+ );
2005
+ });
2006
+
2007
+ // src/factory.tsx
2008
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1698
2009
  var NOOP_CLIENT = {
1699
2010
  trackEvent: () => {
1700
2011
  },
@@ -1704,7 +2015,7 @@ var NOOP_CLIENT = {
1704
2015
  getVisitorId: () => ""
1705
2016
  };
1706
2017
  function createTracking(options) {
1707
- const { apiKey, endpoint, triggers, environment, debug } = options;
2018
+ const { apiKey, endpoint, triggers, environment, debug, phone } = options;
1708
2019
  if (!apiKey || !endpoint) {
1709
2020
  if (apiKey || endpoint) {
1710
2021
  console.warn(
@@ -1713,14 +2024,16 @@ function createTracking(options) {
1713
2024
  }
1714
2025
  const noopTyped = NOOP_CLIENT;
1715
2026
  return {
1716
- TrackingProvider: ({ children }) => children,
2027
+ // Still publish phone config so usePhoneField/<PhoneField> work even when
2028
+ // tracking is disabled (missing apiKey/endpoint).
2029
+ TrackingProvider: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PhoneConfigProvider, { value: phone ?? null, children }),
1717
2030
  useTracking: () => noopTyped
1718
2031
  };
1719
2032
  }
1720
- const TrackingContext = (0, import_react4.createContext)(null);
2033
+ const TrackingContext = (0, import_react5.createContext)(null);
1721
2034
  function TrackingProvider({ gtagId, gtagIds, children }) {
1722
2035
  const resolvedGtagIds = gtagIds ?? (gtagId ? { default: gtagId } : void 0);
1723
- const client = (0, import_react4.useMemo)(
2036
+ const client = (0, import_react5.useMemo)(
1724
2037
  () => createTypedClient(
1725
2038
  getOrCreateTrackingClient({
1726
2039
  apiKey,
@@ -1738,15 +2051,15 @@ function createTracking(options) {
1738
2051
  ),
1739
2052
  []
1740
2053
  );
1741
- const gtagIdsKey = (0, import_react4.useMemo)(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
1742
- (0, import_react4.useEffect)(() => {
2054
+ const gtagIdsKey = (0, import_react5.useMemo)(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
2055
+ (0, import_react5.useEffect)(() => {
1743
2056
  if (gtagIds && Object.keys(gtagIds).length > 0) {
1744
2057
  bootstrapMultipleGtags(gtagIds);
1745
2058
  } else if (gtagId) {
1746
2059
  bootstrapGoogleAdsTracking(gtagId);
1747
2060
  }
1748
2061
  }, [gtagId, gtagIdsKey]);
1749
- (0, import_react4.useEffect)(() => {
2062
+ (0, import_react5.useEffect)(() => {
1750
2063
  const detachers = [];
1751
2064
  const rawClient = getOrCreateTrackingClient({
1752
2065
  apiKey,
@@ -1786,10 +2099,10 @@ function createTracking(options) {
1786
2099
  }
1787
2100
  };
1788
2101
  }, []);
1789
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrackingContext.Provider, { value: client, children });
2102
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrackingContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PhoneConfigProvider, { value: phone ?? null, children }) });
1790
2103
  }
1791
2104
  function useTracking() {
1792
- const client = (0, import_react4.useContext)(TrackingContext);
2105
+ const client = (0, import_react5.useContext)(TrackingContext);
1793
2106
  if (client === null) {
1794
2107
  throw new Error(
1795
2108
  "useTracking must be called inside a <TrackingProvider> returned by createTracking()"
@@ -1803,8 +2116,13 @@ function createTracking(options) {
1803
2116
  0 && (module.exports = {
1804
2117
  AranovaApiError,
1805
2118
  ConsentBanner,
2119
+ DEFAULT_PHONE_COUNTRY,
1806
2120
  GoogleAdsTracking,
2121
+ NAMED_RANGES,
2122
+ PhoneField,
2123
+ SUPPORTED_CURRENCIES,
1807
2124
  TRACKING_PARAM_KEYS,
2125
+ TRACKING_RANGES,
1808
2126
  captureTrackingParamsFromLocation,
1809
2127
  createSalesClient,
1810
2128
  createTracking,
@@ -1812,15 +2130,28 @@ function createTracking(options) {
1812
2130
  createTrackingEventCreatePayload,
1813
2131
  createTrackingSessionUpsertPayload,
1814
2132
  fetchServices,
2133
+ formatDateInTz,
1815
2134
  formatMoney,
2135
+ formatPhone,
2136
+ formatPhoneAsTyped,
1816
2137
  fromMinor,
1817
2138
  getConsentState,
2139
+ parsePhone,
2140
+ phoneField,
1818
2141
  resetConsent,
2142
+ saleCreateSchema,
2143
+ saleItemSchema,
2144
+ saleServiceSchema,
2145
+ saleUpdateSchema,
2146
+ salesRequest,
1819
2147
  setConsentState,
2148
+ toE164,
1820
2149
  toMinor,
1821
2150
  useConsent,
1822
2151
  useConsentState,
1823
2152
  useGclid,
2153
+ usePhoneConfig,
2154
+ usePhoneField,
1824
2155
  useTrackingParams
1825
2156
  });
1826
2157
  //# sourceMappingURL=index.js.map