@callforge/tracking-client 0.9.2 → 0.10.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/README.md CHANGED
@@ -19,18 +19,17 @@ Client integration requirements:
19
19
  - Keep handling `phoneNumber` and `leaseId` separately. A request can return a phone number with `leaseId: null` when lease assignment is intentionally suppressed.
20
20
  - For attribution/scale metrics, treat `leaseId` as the source of truth for lease-backed traffic.
21
21
 
22
- ## Realtime Data Layer Upgrade (v0.9+)
22
+ ## Realtime Data Layer Upgrade (v0.10+)
23
23
 
24
24
  This release adds explicit browser helpers for the new realtime layer:
25
- - `client.linkPhoneCall({ phoneNumber })` for strict web-click to phone-call linkage.
26
- - `client.setWebZip(zipCode, { source })` for immediate web ZIP availability to call processing.
25
+ - `client.linkPhoneCall({ phoneNumber, realtime })` for strict web-click to phone-call linkage plus optional realtime fields.
27
26
 
28
27
  Integration checklist:
29
28
  - Call `linkPhoneCall` immediately before opening a `tel:` link.
30
29
  - Pass the exact dialed number string (`+1...`) used by the link.
31
30
  - Continue dialing even if `linkPhoneCall` fails (best-effort attribution assist, not UX-blocking).
32
- - Call `setWebZip` when the visitor selects or types a ZIP.
33
- - Keep `setParams` for broader attribution params; use `setWebZip` for fast ZIP propagation.
31
+ - When ZIP is known, include it in `linkPhoneCall(...realtime)` as `webZip` and `webZipSource`.
32
+ - Use `realtime.params` for additional key/value fields you want persisted with the linked session.
34
33
 
35
34
  ## Quick Start
36
35
 
@@ -97,32 +96,40 @@ Notes:
97
96
  - `callIntentToken` is short-lived and single-use.
98
97
  - Treat it as an opaque secret (do not log it).
99
98
 
100
- ### 4. Realtime call-link + web ZIP helpers (recommended)
99
+ ### 4. Realtime call-link with bundled realtime fields (recommended)
101
100
 
102
- Use explicit helpers to write realtime data from the browser.
101
+ Use `linkPhoneCall` to create the strict call-link and optionally attach realtime fields in the same request.
103
102
 
104
103
  ```typescript
105
104
  const client = CallForge.init({ categoryId: 'your-category-id' });
106
105
 
107
- async function dialTrackedNumber(phoneNumber: string) {
106
+ async function dialTrackedNumber(phoneNumber: string, webZip?: string) {
108
107
  try {
109
108
  // Default TTL is 30s (clamped server-side to 5-120s).
110
- await client.linkPhoneCall({ phoneNumber });
109
+ await client.linkPhoneCall({
110
+ phoneNumber,
111
+ realtime: webZip
112
+ ? {
113
+ webZip,
114
+ webZipSource: 'manual', // or 'suggested'
115
+ params: {
116
+ zipChoiceMethod: 'picker',
117
+ },
118
+ }
119
+ : undefined,
120
+ });
111
121
  } finally {
112
122
  // Do not block dialing on telemetry failure.
113
123
  window.location.href = `tel:${phoneNumber}`;
114
124
  }
115
125
  }
116
-
117
- // Example ZIP picker handler
118
- await client.setWebZip('30309', { source: 'manual' });
119
126
  ```
120
127
 
121
128
  Notes:
122
129
  - `linkPhoneCall` is optimized for mobile tap-to-call timing.
123
- - `setWebZip` accepts `manual` (typed) and `suggested` (picked from options).
124
- - `setWebZip` validates ZIP format (`12345`) client-side before sending.
125
- - `setWebZip` also sends `webZip` as a custom session param in a best-effort sync backup path.
130
+ - `realtime.webZip` accepts `manual` (typed) and `suggested` (picked from options) via `webZipSource`.
131
+ - `realtime.webZip` is validated client-side (`12345` format).
132
+ - `realtime.params` allows additional key/value payloads for future realtime enrichment without API changes.
126
133
 
127
134
  ### 5. GA4 Integration
128
135
 
@@ -256,12 +263,19 @@ console.log(intent.callIntentToken);
256
263
 
257
264
  ### `client.linkPhoneCall(input)`
258
265
 
259
- Create a short-lived realtime call-link intent keyed by dialed number.
266
+ Create a short-lived realtime call-link intent keyed by dialed number, with optional realtime payload fields persisted against the linked session.
260
267
 
261
268
  ```typescript
262
269
  const result = await client.linkPhoneCall({
263
270
  phoneNumber: '+13105551234',
264
271
  ttlSeconds: 30, // Optional, default 30s
272
+ realtime: {
273
+ webZip: '30309', // Optional
274
+ webZipSource: 'manual', // Optional, defaults to 'manual' when webZip is provided
275
+ params: { // Optional
276
+ zipChoiceMethod: 'picker',
277
+ },
278
+ },
265
279
  });
266
280
 
267
281
  console.log(result.status); // 'ready'
@@ -269,25 +283,10 @@ console.log(result.status); // 'ready'
269
283
 
270
284
  Use this right before `tel:` navigation so inbound call handling can perform strict 1:1 consume.
271
285
 
272
- ### `client.setWebZip(zipCode, options?)`
273
-
274
- Store visitor-selected ZIP in the realtime profile layer for immediate call-side enrichment.
275
-
276
- ```typescript
277
- const result = await client.setWebZip('30309', {
278
- source: 'manual', // or 'suggested'
279
- ttlSeconds: 3600, // Optional
280
- });
281
-
282
- if (result.status === 'ready') {
283
- console.log(result.profile.webZipCode);
284
- }
285
- ```
286
-
287
286
  Behavior:
288
- - Rejects invalid ZIP values unless they are 5 digits.
289
- - Writes profile data keyed by `sessionId`.
290
- - Also mirrors `webZip` and `webZipSource` into custom params and triggers a best-effort session sync backup.
287
+ - Rejects invalid `realtime.webZip` values unless they are 5 digits.
288
+ - Persists bundled realtime values (`webZip` + `params`) during call-link intent creation.
289
+ - Mirrors bundled realtime values into custom params and performs a best-effort session sync backup.
291
290
 
292
291
  ### `client.onReady(callback)`
293
292
 
@@ -401,7 +400,8 @@ import type {
401
400
  LocationReadyCallback,
402
401
  CallIntentResponse,
403
402
  CallLinkIntentResponse,
404
- RealtimeProfileResponse,
403
+ LinkPhoneCallInput,
404
+ RealtimeLinkPayload,
405
405
  RealtimeProfileSource,
406
406
  } from '@callforge/tracking-client';
407
407
  ```
package/dist/index.d.mts CHANGED
@@ -92,18 +92,26 @@ interface CallLinkIntentResponse {
92
92
  expiresAt: string;
93
93
  }
94
94
  type RealtimeProfileSource = 'manual' | 'suggested';
95
- type RealtimeProfileResponse = {
96
- status: 'ready';
97
- profile: {
98
- sessionId: string;
99
- webZipCode: string;
100
- webZipSource: RealtimeProfileSource;
101
- webZipUpdatedAt: string;
102
- expiresAt: string;
103
- };
104
- } | {
105
- status: 'unavailable';
106
- };
95
+ interface RealtimeLinkPayload {
96
+ /**
97
+ * Optional web ZIP to persist alongside call-link intent.
98
+ * Must be 5 digits.
99
+ */
100
+ webZip?: string;
101
+ /**
102
+ * Source for webZip. Defaults to 'manual' when webZip is provided.
103
+ */
104
+ webZipSource?: RealtimeProfileSource;
105
+ /**
106
+ * Additional realtime params to persist with the linked session.
107
+ */
108
+ params?: Record<string, string>;
109
+ }
110
+ interface LinkPhoneCallInput {
111
+ phoneNumber: string;
112
+ ttlSeconds?: number;
113
+ realtime?: RealtimeLinkPayload;
114
+ }
107
115
  /**
108
116
  * Callback function for onReady subscription.
109
117
  */
@@ -160,17 +168,9 @@ declare class CallForge {
160
168
  * Create a short-lived realtime call-link intent for a specific dialed number.
161
169
  * Use this immediately before opening a `tel:` link.
162
170
  */
163
- linkPhoneCall(input: {
164
- phoneNumber: string;
165
- ttlSeconds?: number;
166
- }): Promise<CallLinkIntentResponse>;
167
- /**
168
- * Write visitor-selected web ZIP to the realtime data layer.
169
- */
170
- setWebZip(zipCode: string, options?: {
171
- source?: RealtimeProfileSource;
172
- ttlSeconds?: number;
173
- }): Promise<RealtimeProfileResponse>;
171
+ linkPhoneCall(input: LinkPhoneCallInput): Promise<CallLinkIntentResponse>;
172
+ private normalizeRealtimeLinkPayload;
173
+ private buildRealtimeFallbackParams;
174
174
  /**
175
175
  * Subscribe to session ready event.
176
176
  * Callback is called once session data is available.
@@ -234,4 +234,4 @@ declare class CallForge {
234
234
  */
235
235
  declare function getPreloadSnippet(config: CallForgeConfig): string;
236
236
 
237
- export { CallForge, type CallForgeConfig, type CallIntentResponse, type CallLinkIntentResponse, type LocationReadyCallback, type ReadyCallback, type RealtimeProfileResponse, type RealtimeProfileSource, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
237
+ export { CallForge, type CallForgeConfig, type CallIntentResponse, type CallLinkIntentResponse, type LinkPhoneCallInput, type LocationReadyCallback, type ReadyCallback, type RealtimeLinkPayload, type RealtimeProfileSource, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
package/dist/index.d.ts CHANGED
@@ -92,18 +92,26 @@ interface CallLinkIntentResponse {
92
92
  expiresAt: string;
93
93
  }
94
94
  type RealtimeProfileSource = 'manual' | 'suggested';
95
- type RealtimeProfileResponse = {
96
- status: 'ready';
97
- profile: {
98
- sessionId: string;
99
- webZipCode: string;
100
- webZipSource: RealtimeProfileSource;
101
- webZipUpdatedAt: string;
102
- expiresAt: string;
103
- };
104
- } | {
105
- status: 'unavailable';
106
- };
95
+ interface RealtimeLinkPayload {
96
+ /**
97
+ * Optional web ZIP to persist alongside call-link intent.
98
+ * Must be 5 digits.
99
+ */
100
+ webZip?: string;
101
+ /**
102
+ * Source for webZip. Defaults to 'manual' when webZip is provided.
103
+ */
104
+ webZipSource?: RealtimeProfileSource;
105
+ /**
106
+ * Additional realtime params to persist with the linked session.
107
+ */
108
+ params?: Record<string, string>;
109
+ }
110
+ interface LinkPhoneCallInput {
111
+ phoneNumber: string;
112
+ ttlSeconds?: number;
113
+ realtime?: RealtimeLinkPayload;
114
+ }
107
115
  /**
108
116
  * Callback function for onReady subscription.
109
117
  */
@@ -160,17 +168,9 @@ declare class CallForge {
160
168
  * Create a short-lived realtime call-link intent for a specific dialed number.
161
169
  * Use this immediately before opening a `tel:` link.
162
170
  */
163
- linkPhoneCall(input: {
164
- phoneNumber: string;
165
- ttlSeconds?: number;
166
- }): Promise<CallLinkIntentResponse>;
167
- /**
168
- * Write visitor-selected web ZIP to the realtime data layer.
169
- */
170
- setWebZip(zipCode: string, options?: {
171
- source?: RealtimeProfileSource;
172
- ttlSeconds?: number;
173
- }): Promise<RealtimeProfileResponse>;
171
+ linkPhoneCall(input: LinkPhoneCallInput): Promise<CallLinkIntentResponse>;
172
+ private normalizeRealtimeLinkPayload;
173
+ private buildRealtimeFallbackParams;
174
174
  /**
175
175
  * Subscribe to session ready event.
176
176
  * Callback is called once session data is available.
@@ -234,4 +234,4 @@ declare class CallForge {
234
234
  */
235
235
  declare function getPreloadSnippet(config: CallForgeConfig): string;
236
236
 
237
- export { CallForge, type CallForgeConfig, type CallIntentResponse, type CallLinkIntentResponse, type LocationReadyCallback, type ReadyCallback, type RealtimeProfileResponse, type RealtimeProfileSource, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
237
+ export { CallForge, type CallForgeConfig, type CallIntentResponse, type CallLinkIntentResponse, type LinkPhoneCallInput, type LocationReadyCallback, type ReadyCallback, type RealtimeLinkPayload, type RealtimeProfileSource, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  var __defProp = Object.defineProperty;
3
- var __defProps = Object.defineProperties;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
6
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
5
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
8
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
@@ -19,7 +17,6 @@ var __spreadValues = (a, b) => {
19
17
  }
20
18
  return a;
21
19
  };
22
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
23
20
  var __export = (target, all) => {
24
21
  for (var name in all)
25
22
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -207,7 +204,6 @@ var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
207
204
  var FETCH_TIMEOUT_MS = 1e4;
208
205
  var CALL_INTENT_TIMEOUT_MS = 8e3;
209
206
  var REALTIME_CALL_LINK_TIMEOUT_MS = 5e3;
210
- var REALTIME_PROFILE_TIMEOUT_MS = 5e3;
211
207
  var BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS = 1e4;
212
208
  var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
213
209
  var ZIP_CODE_PATTERN = /^\d{5}$/;
@@ -301,6 +297,11 @@ var CallForge = class _CallForge {
301
297
  */
302
298
  async linkPhoneCall(input) {
303
299
  const session = await this.getSession();
300
+ const realtime = this.normalizeRealtimeLinkPayload(input.realtime);
301
+ const fallbackParams = this.buildRealtimeFallbackParams(realtime);
302
+ if (Object.keys(fallbackParams).length > 0) {
303
+ void this.setParams(fallbackParams);
304
+ }
304
305
  const controller = new AbortController();
305
306
  const timeoutId = setTimeout(() => controller.abort(), REALTIME_CALL_LINK_TIMEOUT_MS);
306
307
  try {
@@ -311,11 +312,11 @@ var CallForge = class _CallForge {
311
312
  },
312
313
  credentials: "omit",
313
314
  signal: controller.signal,
314
- body: JSON.stringify({
315
+ body: JSON.stringify(__spreadValues({
315
316
  sessionToken: session.sessionToken,
316
317
  phoneNumber: input.phoneNumber,
317
318
  ttlSeconds: input.ttlSeconds
318
- })
319
+ }, realtime ? { realtime } : {}))
319
320
  });
320
321
  if (!response.ok) {
321
322
  throw new Error(`Realtime call-link API error: ${response.status} ${response.statusText}`);
@@ -325,52 +326,59 @@ var CallForge = class _CallForge {
325
326
  clearTimeout(timeoutId);
326
327
  }
327
328
  }
328
- /**
329
- * Write visitor-selected web ZIP to the realtime data layer.
330
- */
331
- async setWebZip(zipCode, options) {
332
- var _a;
333
- const normalizedZip = zipCode.trim();
334
- if (!ZIP_CODE_PATTERN.test(normalizedZip)) {
335
- throw new Error("Invalid ZIP code. Expected a 5-digit ZIP.");
329
+ normalizeRealtimeLinkPayload(realtime) {
330
+ if (!realtime) {
331
+ return void 0;
336
332
  }
337
- const session = await this.getSession();
338
- const fallbackSource = (_a = options == null ? void 0 : options.source) != null ? _a : "manual";
339
- void this.setParams({
340
- webZip: normalizedZip,
341
- webZipSource: fallbackSource
342
- });
343
- const controller = new AbortController();
344
- const timeoutId = setTimeout(() => controller.abort(), REALTIME_PROFILE_TIMEOUT_MS);
345
- try {
346
- const response = await fetch(`${this.config.endpoint}/v1/tracking/realtime-profile`, {
347
- method: "POST",
348
- headers: {
349
- "Content-Type": "application/json"
350
- },
351
- credentials: "omit",
352
- signal: controller.signal,
353
- body: JSON.stringify({
354
- sessionToken: session.sessionToken,
355
- webZip: normalizedZip,
356
- source: options == null ? void 0 : options.source,
357
- ttlSeconds: options == null ? void 0 : options.ttlSeconds
358
- })
359
- });
360
- if (!response.ok) {
361
- throw new Error(`Realtime profile API error: ${response.status} ${response.statusText}`);
333
+ const normalized = {};
334
+ if (typeof realtime.webZip !== "undefined") {
335
+ const normalizedZip = realtime.webZip.trim();
336
+ if (!ZIP_CODE_PATTERN.test(normalizedZip)) {
337
+ throw new Error("Invalid realtime.webZip. Expected a 5-digit ZIP.");
362
338
  }
363
- const result = await response.json();
364
- if (result.status === "ready") {
365
- this.customParams = __spreadProps(__spreadValues({}, this.customParams), {
366
- webZip: result.profile.webZipCode,
367
- webZipSource: result.profile.webZipSource
368
- });
339
+ normalized.webZip = normalizedZip;
340
+ }
341
+ if (typeof realtime.webZipSource !== "undefined") {
342
+ if (realtime.webZipSource !== "manual" && realtime.webZipSource !== "suggested") {
343
+ throw new Error('Invalid realtime.webZipSource. Expected "manual" or "suggested".');
369
344
  }
370
- return result;
371
- } finally {
372
- clearTimeout(timeoutId);
345
+ normalized.webZipSource = realtime.webZipSource;
346
+ }
347
+ if (normalized.webZipSource && !normalized.webZip) {
348
+ throw new Error("realtime.webZipSource requires realtime.webZip.");
373
349
  }
350
+ if (realtime.params) {
351
+ const normalizedParams = {};
352
+ for (const [key, value] of Object.entries(realtime.params)) {
353
+ if (key.trim() === "") {
354
+ throw new Error("Invalid realtime.params key. Keys must be non-empty strings.");
355
+ }
356
+ if (typeof value !== "string") {
357
+ throw new Error(`Invalid realtime.params value for key "${key}". Expected string.`);
358
+ }
359
+ normalizedParams[key] = value;
360
+ }
361
+ if (Object.keys(normalizedParams).length > 0) {
362
+ normalized.params = normalizedParams;
363
+ }
364
+ }
365
+ if (normalized.webZip && !normalized.webZipSource) {
366
+ normalized.webZipSource = "manual";
367
+ }
368
+ if (typeof normalized.webZip === "undefined" && typeof normalized.webZipSource === "undefined" && typeof normalized.params === "undefined") {
369
+ return void 0;
370
+ }
371
+ return normalized;
372
+ }
373
+ buildRealtimeFallbackParams(realtime) {
374
+ var _a, _b;
375
+ if (!realtime) {
376
+ return {};
377
+ }
378
+ return __spreadValues(__spreadValues({}, (_a = realtime.params) != null ? _a : {}), realtime.webZip ? {
379
+ webZip: realtime.webZip,
380
+ webZipSource: (_b = realtime.webZipSource) != null ? _b : "manual"
381
+ } : {});
374
382
  }
375
383
  /**
376
384
  * Subscribe to session ready event.
@@ -553,11 +561,8 @@ var CallForge = class _CallForge {
553
561
  async fetchFromApi(locationId, sessionToken, params) {
554
562
  const controller = new AbortController();
555
563
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
556
- let didForceRefreshBootstrap = false;
557
- let usedBootstrapTokenInInitialRequest = false;
558
564
  try {
559
565
  let bootstrapToken = this.getCachedBootstrapToken();
560
- usedBootstrapTokenInInitialRequest = !!bootstrapToken;
561
566
  let response = await fetch(
562
567
  this.buildUrl(locationId, sessionToken, params, bootstrapToken),
563
568
  {
@@ -567,7 +572,6 @@ var CallForge = class _CallForge {
567
572
  );
568
573
  if (response.status === 401) {
569
574
  bootstrapToken = await this.getBootstrapToken(true);
570
- didForceRefreshBootstrap = true;
571
575
  if (bootstrapToken) {
572
576
  response = await fetch(
573
577
  this.buildUrl(locationId, sessionToken, params, bootstrapToken),
@@ -581,24 +585,7 @@ var CallForge = class _CallForge {
581
585
  if (!response.ok) {
582
586
  throw new Error(`API error: ${response.status} ${response.statusText}`);
583
587
  }
584
- let data = await response.json();
585
- if (data.leaseId === null && !usedBootstrapTokenInInitialRequest && !didForceRefreshBootstrap) {
586
- const refreshedBootstrapToken = await this.getBootstrapToken(true);
587
- didForceRefreshBootstrap = true;
588
- if (refreshedBootstrapToken) {
589
- const retryResponse = await fetch(
590
- this.buildUrl(locationId, sessionToken, params, refreshedBootstrapToken),
591
- {
592
- credentials: "omit",
593
- signal: controller.signal
594
- }
595
- );
596
- if (retryResponse.ok) {
597
- data = await retryResponse.json();
598
- }
599
- }
600
- }
601
- return data;
588
+ return await response.json();
602
589
  } finally {
603
590
  clearTimeout(timeoutId);
604
591
  }
package/dist/index.mjs CHANGED
@@ -1,6 +1,4 @@
1
1
  var __defProp = Object.defineProperty;
2
- var __defProps = Object.defineProperties;
3
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
2
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
3
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
4
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -16,7 +14,6 @@ var __spreadValues = (a, b) => {
16
14
  }
17
15
  return a;
18
16
  };
19
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
17
 
21
18
  // src/cache.ts
22
19
  var EXPIRY_BUFFER_MS = 3e4;
@@ -183,7 +180,6 @@ var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
183
180
  var FETCH_TIMEOUT_MS = 1e4;
184
181
  var CALL_INTENT_TIMEOUT_MS = 8e3;
185
182
  var REALTIME_CALL_LINK_TIMEOUT_MS = 5e3;
186
- var REALTIME_PROFILE_TIMEOUT_MS = 5e3;
187
183
  var BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS = 1e4;
188
184
  var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
189
185
  var ZIP_CODE_PATTERN = /^\d{5}$/;
@@ -277,6 +273,11 @@ var CallForge = class _CallForge {
277
273
  */
278
274
  async linkPhoneCall(input) {
279
275
  const session = await this.getSession();
276
+ const realtime = this.normalizeRealtimeLinkPayload(input.realtime);
277
+ const fallbackParams = this.buildRealtimeFallbackParams(realtime);
278
+ if (Object.keys(fallbackParams).length > 0) {
279
+ void this.setParams(fallbackParams);
280
+ }
280
281
  const controller = new AbortController();
281
282
  const timeoutId = setTimeout(() => controller.abort(), REALTIME_CALL_LINK_TIMEOUT_MS);
282
283
  try {
@@ -287,11 +288,11 @@ var CallForge = class _CallForge {
287
288
  },
288
289
  credentials: "omit",
289
290
  signal: controller.signal,
290
- body: JSON.stringify({
291
+ body: JSON.stringify(__spreadValues({
291
292
  sessionToken: session.sessionToken,
292
293
  phoneNumber: input.phoneNumber,
293
294
  ttlSeconds: input.ttlSeconds
294
- })
295
+ }, realtime ? { realtime } : {}))
295
296
  });
296
297
  if (!response.ok) {
297
298
  throw new Error(`Realtime call-link API error: ${response.status} ${response.statusText}`);
@@ -301,52 +302,59 @@ var CallForge = class _CallForge {
301
302
  clearTimeout(timeoutId);
302
303
  }
303
304
  }
304
- /**
305
- * Write visitor-selected web ZIP to the realtime data layer.
306
- */
307
- async setWebZip(zipCode, options) {
308
- var _a;
309
- const normalizedZip = zipCode.trim();
310
- if (!ZIP_CODE_PATTERN.test(normalizedZip)) {
311
- throw new Error("Invalid ZIP code. Expected a 5-digit ZIP.");
305
+ normalizeRealtimeLinkPayload(realtime) {
306
+ if (!realtime) {
307
+ return void 0;
312
308
  }
313
- const session = await this.getSession();
314
- const fallbackSource = (_a = options == null ? void 0 : options.source) != null ? _a : "manual";
315
- void this.setParams({
316
- webZip: normalizedZip,
317
- webZipSource: fallbackSource
318
- });
319
- const controller = new AbortController();
320
- const timeoutId = setTimeout(() => controller.abort(), REALTIME_PROFILE_TIMEOUT_MS);
321
- try {
322
- const response = await fetch(`${this.config.endpoint}/v1/tracking/realtime-profile`, {
323
- method: "POST",
324
- headers: {
325
- "Content-Type": "application/json"
326
- },
327
- credentials: "omit",
328
- signal: controller.signal,
329
- body: JSON.stringify({
330
- sessionToken: session.sessionToken,
331
- webZip: normalizedZip,
332
- source: options == null ? void 0 : options.source,
333
- ttlSeconds: options == null ? void 0 : options.ttlSeconds
334
- })
335
- });
336
- if (!response.ok) {
337
- throw new Error(`Realtime profile API error: ${response.status} ${response.statusText}`);
309
+ const normalized = {};
310
+ if (typeof realtime.webZip !== "undefined") {
311
+ const normalizedZip = realtime.webZip.trim();
312
+ if (!ZIP_CODE_PATTERN.test(normalizedZip)) {
313
+ throw new Error("Invalid realtime.webZip. Expected a 5-digit ZIP.");
338
314
  }
339
- const result = await response.json();
340
- if (result.status === "ready") {
341
- this.customParams = __spreadProps(__spreadValues({}, this.customParams), {
342
- webZip: result.profile.webZipCode,
343
- webZipSource: result.profile.webZipSource
344
- });
315
+ normalized.webZip = normalizedZip;
316
+ }
317
+ if (typeof realtime.webZipSource !== "undefined") {
318
+ if (realtime.webZipSource !== "manual" && realtime.webZipSource !== "suggested") {
319
+ throw new Error('Invalid realtime.webZipSource. Expected "manual" or "suggested".');
345
320
  }
346
- return result;
347
- } finally {
348
- clearTimeout(timeoutId);
321
+ normalized.webZipSource = realtime.webZipSource;
322
+ }
323
+ if (normalized.webZipSource && !normalized.webZip) {
324
+ throw new Error("realtime.webZipSource requires realtime.webZip.");
349
325
  }
326
+ if (realtime.params) {
327
+ const normalizedParams = {};
328
+ for (const [key, value] of Object.entries(realtime.params)) {
329
+ if (key.trim() === "") {
330
+ throw new Error("Invalid realtime.params key. Keys must be non-empty strings.");
331
+ }
332
+ if (typeof value !== "string") {
333
+ throw new Error(`Invalid realtime.params value for key "${key}". Expected string.`);
334
+ }
335
+ normalizedParams[key] = value;
336
+ }
337
+ if (Object.keys(normalizedParams).length > 0) {
338
+ normalized.params = normalizedParams;
339
+ }
340
+ }
341
+ if (normalized.webZip && !normalized.webZipSource) {
342
+ normalized.webZipSource = "manual";
343
+ }
344
+ if (typeof normalized.webZip === "undefined" && typeof normalized.webZipSource === "undefined" && typeof normalized.params === "undefined") {
345
+ return void 0;
346
+ }
347
+ return normalized;
348
+ }
349
+ buildRealtimeFallbackParams(realtime) {
350
+ var _a, _b;
351
+ if (!realtime) {
352
+ return {};
353
+ }
354
+ return __spreadValues(__spreadValues({}, (_a = realtime.params) != null ? _a : {}), realtime.webZip ? {
355
+ webZip: realtime.webZip,
356
+ webZipSource: (_b = realtime.webZipSource) != null ? _b : "manual"
357
+ } : {});
350
358
  }
351
359
  /**
352
360
  * Subscribe to session ready event.
@@ -529,11 +537,8 @@ var CallForge = class _CallForge {
529
537
  async fetchFromApi(locationId, sessionToken, params) {
530
538
  const controller = new AbortController();
531
539
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
532
- let didForceRefreshBootstrap = false;
533
- let usedBootstrapTokenInInitialRequest = false;
534
540
  try {
535
541
  let bootstrapToken = this.getCachedBootstrapToken();
536
- usedBootstrapTokenInInitialRequest = !!bootstrapToken;
537
542
  let response = await fetch(
538
543
  this.buildUrl(locationId, sessionToken, params, bootstrapToken),
539
544
  {
@@ -543,7 +548,6 @@ var CallForge = class _CallForge {
543
548
  );
544
549
  if (response.status === 401) {
545
550
  bootstrapToken = await this.getBootstrapToken(true);
546
- didForceRefreshBootstrap = true;
547
551
  if (bootstrapToken) {
548
552
  response = await fetch(
549
553
  this.buildUrl(locationId, sessionToken, params, bootstrapToken),
@@ -557,24 +561,7 @@ var CallForge = class _CallForge {
557
561
  if (!response.ok) {
558
562
  throw new Error(`API error: ${response.status} ${response.statusText}`);
559
563
  }
560
- let data = await response.json();
561
- if (data.leaseId === null && !usedBootstrapTokenInInitialRequest && !didForceRefreshBootstrap) {
562
- const refreshedBootstrapToken = await this.getBootstrapToken(true);
563
- didForceRefreshBootstrap = true;
564
- if (refreshedBootstrapToken) {
565
- const retryResponse = await fetch(
566
- this.buildUrl(locationId, sessionToken, params, refreshedBootstrapToken),
567
- {
568
- credentials: "omit",
569
- signal: controller.signal
570
- }
571
- );
572
- if (retryResponse.ok) {
573
- data = await retryResponse.json();
574
- }
575
- }
576
- }
577
- return data;
564
+ return await response.json();
578
565
  } finally {
579
566
  clearTimeout(timeoutId);
580
567
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callforge/tracking-client",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",