@callforge/tracking-client 0.7.1 → 0.9.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 +93 -5
- package/dist/index.d.mts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +175 -5
- package/dist/index.mjs +175 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @callforge/tracking-client
|
|
2
2
|
|
|
3
|
-
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and
|
|
3
|
+
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,11 +10,33 @@ npm install @callforge/tracking-client
|
|
|
10
10
|
pnpm add @callforge/tracking-client
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Lease Hardening Migration (v0.8+)
|
|
14
|
+
|
|
15
|
+
This release adds bootstrap-token support for lease hardening and bot suppression.
|
|
16
|
+
|
|
17
|
+
Client integration requirements:
|
|
18
|
+
- Add the generated preload snippet in `<head>`. This prefetches bootstrap tokens and keeps session lookup fast.
|
|
19
|
+
- Keep handling `phoneNumber` and `leaseId` separately. A request can return a phone number with `leaseId: null` when lease assignment is intentionally suppressed.
|
|
20
|
+
- For attribution/scale metrics, treat `leaseId` as the source of truth for lease-backed traffic.
|
|
21
|
+
|
|
22
|
+
## Realtime Data Layer Upgrade (v0.9+)
|
|
23
|
+
|
|
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.
|
|
27
|
+
|
|
28
|
+
Integration checklist:
|
|
29
|
+
- Call `linkPhoneCall` immediately before opening a `tel:` link.
|
|
30
|
+
- Pass the exact dialed number string (`+1...`) used by the link.
|
|
31
|
+
- 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.
|
|
34
|
+
|
|
13
35
|
## Quick Start
|
|
14
36
|
|
|
15
|
-
### 1. Add preload snippet to `<head>` (
|
|
37
|
+
### 1. Add preload snippet to `<head>` (required for deterministic leases)
|
|
16
38
|
|
|
17
|
-
For optimal performance
|
|
39
|
+
For optimal performance and lease hardening, add this snippet to your HTML `<head>`:
|
|
18
40
|
|
|
19
41
|
```typescript
|
|
20
42
|
import { getPreloadSnippet } from '@callforge/tracking-client';
|
|
@@ -75,7 +97,33 @@ Notes:
|
|
|
75
97
|
- `callIntentToken` is short-lived and single-use.
|
|
76
98
|
- Treat it as an opaque secret (do not log it).
|
|
77
99
|
|
|
78
|
-
### 4.
|
|
100
|
+
### 4. Realtime call-link + web ZIP helpers (recommended)
|
|
101
|
+
|
|
102
|
+
Use explicit helpers to write realtime data from the browser.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const client = CallForge.init({ categoryId: 'your-category-id' });
|
|
106
|
+
|
|
107
|
+
async function dialTrackedNumber(phoneNumber: string) {
|
|
108
|
+
try {
|
|
109
|
+
// Default TTL is 30s (clamped server-side to 5-120s).
|
|
110
|
+
await client.linkPhoneCall({ phoneNumber });
|
|
111
|
+
} finally {
|
|
112
|
+
// Do not block dialing on telemetry failure.
|
|
113
|
+
window.location.href = `tel:${phoneNumber}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Example ZIP picker handler
|
|
118
|
+
await client.setWebZip('30309', { source: 'manual' });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Notes:
|
|
122
|
+
- `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
|
+
|
|
126
|
+
### 5. GA4 Integration
|
|
79
127
|
|
|
80
128
|
To enable GA4 call event tracking, CallForge needs the GA4 `client_id` for the visitor (from the `_ga` cookie).
|
|
81
129
|
Optionally provide your GA4 Measurement ID to improve client ID capture reliability when Google Analytics loads late.
|
|
@@ -105,7 +153,7 @@ client.setParams({
|
|
|
105
153
|
});
|
|
106
154
|
```
|
|
107
155
|
|
|
108
|
-
###
|
|
156
|
+
### 6. Track conversion parameters (optional)
|
|
109
157
|
|
|
110
158
|
The client automatically captures ad platform click IDs from the URL:
|
|
111
159
|
|
|
@@ -158,6 +206,7 @@ Behavior:
|
|
|
158
206
|
- Returns cached data if valid.
|
|
159
207
|
- Fetches fresh data when cache is missing/expired.
|
|
160
208
|
- If `loc_physical_ms` is present in the URL, cached sessions are only reused when it matches the cached `locId`.
|
|
209
|
+
- If lease assignment is suppressed (for example bot traffic or missing/invalid bootstrap), `phoneNumber` may be present while `leaseId` is `null`.
|
|
161
210
|
- Throws on network errors or API errors.
|
|
162
211
|
|
|
163
212
|
### `client.getLocation()`
|
|
@@ -204,6 +253,41 @@ const intent = await client.createCallIntent();
|
|
|
204
253
|
console.log(intent.callIntentToken);
|
|
205
254
|
```
|
|
206
255
|
|
|
256
|
+
### `client.linkPhoneCall(input)`
|
|
257
|
+
|
|
258
|
+
Create a short-lived realtime call-link intent keyed by dialed number.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const result = await client.linkPhoneCall({
|
|
262
|
+
phoneNumber: '+13105551234',
|
|
263
|
+
ttlSeconds: 30, // Optional, default 30s
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
console.log(result.status); // 'ready'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Use this right before `tel:` navigation so inbound call handling can perform strict 1:1 consume.
|
|
270
|
+
|
|
271
|
+
### `client.setWebZip(zipCode, options?)`
|
|
272
|
+
|
|
273
|
+
Store visitor-selected ZIP in the realtime profile layer for immediate call-side enrichment.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const result = await client.setWebZip('30309', {
|
|
277
|
+
source: 'manual', // or 'suggested'
|
|
278
|
+
ttlSeconds: 3600, // Optional
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (result.status === 'ready') {
|
|
282
|
+
console.log(result.profile.webZipCode);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Behavior:
|
|
287
|
+
- Rejects invalid ZIP values unless they are 5 digits.
|
|
288
|
+
- Writes profile data keyed by `sessionId`.
|
|
289
|
+
- Also mirrors `webZip` and `webZipSource` into queued params for subsequent session refreshes.
|
|
290
|
+
|
|
207
291
|
### `client.onReady(callback)`
|
|
208
292
|
|
|
209
293
|
Subscribe to session ready event. Callback is called once session data is available.
|
|
@@ -282,6 +366,7 @@ Parameters are sent as a sorted query string for cache consistency:
|
|
|
282
366
|
|
|
283
367
|
- Session cache key: `cf_tracking_v1_<siteKey>_<categoryId>`
|
|
284
368
|
- Location cache key: `cf_location_v1_<siteKey>`
|
|
369
|
+
- Bootstrap cache key: `cf_bootstrap_v1_<siteKey>_<categoryId>`
|
|
285
370
|
- TTL: controlled by the server `expiresAt` response (currently 30 minutes)
|
|
286
371
|
- Storage: localStorage (falls back to memory if unavailable)
|
|
287
372
|
|
|
@@ -314,6 +399,9 @@ import type {
|
|
|
314
399
|
ReadyCallback,
|
|
315
400
|
LocationReadyCallback,
|
|
316
401
|
CallIntentResponse,
|
|
402
|
+
CallLinkIntentResponse,
|
|
403
|
+
RealtimeProfileResponse,
|
|
404
|
+
RealtimeProfileSource,
|
|
317
405
|
} from '@callforge/tracking-client';
|
|
318
406
|
```
|
|
319
407
|
|
package/dist/index.d.mts
CHANGED
|
@@ -83,6 +83,27 @@ interface CallIntentResponse {
|
|
|
83
83
|
expiresAt: string;
|
|
84
84
|
attributionVersion: 'v1';
|
|
85
85
|
}
|
|
86
|
+
interface CallLinkIntentResponse {
|
|
87
|
+
status: 'ready';
|
|
88
|
+
intentId: string;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
categoryId: string;
|
|
91
|
+
phoneNumber: string;
|
|
92
|
+
expiresAt: string;
|
|
93
|
+
}
|
|
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
|
+
};
|
|
86
107
|
/**
|
|
87
108
|
* Callback function for onReady subscription.
|
|
88
109
|
*/
|
|
@@ -103,6 +124,8 @@ declare class CallForge {
|
|
|
103
124
|
private readonly config;
|
|
104
125
|
private readonly cache;
|
|
105
126
|
private readonly locationCache;
|
|
127
|
+
private readonly bootstrapCacheKey;
|
|
128
|
+
private bootstrapMemoryCache;
|
|
106
129
|
private sessionPromise;
|
|
107
130
|
private locationPromise;
|
|
108
131
|
private customParams;
|
|
@@ -133,6 +156,21 @@ declare class CallForge {
|
|
|
133
156
|
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
134
157
|
*/
|
|
135
158
|
createCallIntent(): Promise<CallIntentResponse>;
|
|
159
|
+
/**
|
|
160
|
+
* Create a short-lived realtime call-link intent for a specific dialed number.
|
|
161
|
+
* Use this immediately before opening a `tel:` link.
|
|
162
|
+
*/
|
|
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>;
|
|
136
174
|
/**
|
|
137
175
|
* Subscribe to session ready event.
|
|
138
176
|
* Callback is called once session data is available.
|
|
@@ -174,7 +212,10 @@ declare class CallForge {
|
|
|
174
212
|
private getLocationId;
|
|
175
213
|
private getAutoParams;
|
|
176
214
|
private fetchFromApi;
|
|
215
|
+
private getBootstrapToken;
|
|
177
216
|
private fetchLocationFromApi;
|
|
217
|
+
private getCachedBootstrapToken;
|
|
218
|
+
private saveBootstrapToken;
|
|
178
219
|
private saveToCache;
|
|
179
220
|
private saveLocationToCache;
|
|
180
221
|
private toTrackingLocation;
|
|
@@ -183,6 +224,7 @@ declare class CallForge {
|
|
|
183
224
|
private formatApiResponse;
|
|
184
225
|
private syncParamsToCallForgeIfPossible;
|
|
185
226
|
private buildUrl;
|
|
227
|
+
private buildBootstrapUrl;
|
|
186
228
|
private buildLocationUrl;
|
|
187
229
|
}
|
|
188
230
|
|
|
@@ -192,4 +234,4 @@ declare class CallForge {
|
|
|
192
234
|
*/
|
|
193
235
|
declare function getPreloadSnippet(config: CallForgeConfig): string;
|
|
194
236
|
|
|
195
|
-
export { CallForge, type CallForgeConfig, type CallIntentResponse, type LocationReadyCallback, type ReadyCallback, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -83,6 +83,27 @@ interface CallIntentResponse {
|
|
|
83
83
|
expiresAt: string;
|
|
84
84
|
attributionVersion: 'v1';
|
|
85
85
|
}
|
|
86
|
+
interface CallLinkIntentResponse {
|
|
87
|
+
status: 'ready';
|
|
88
|
+
intentId: string;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
categoryId: string;
|
|
91
|
+
phoneNumber: string;
|
|
92
|
+
expiresAt: string;
|
|
93
|
+
}
|
|
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
|
+
};
|
|
86
107
|
/**
|
|
87
108
|
* Callback function for onReady subscription.
|
|
88
109
|
*/
|
|
@@ -103,6 +124,8 @@ declare class CallForge {
|
|
|
103
124
|
private readonly config;
|
|
104
125
|
private readonly cache;
|
|
105
126
|
private readonly locationCache;
|
|
127
|
+
private readonly bootstrapCacheKey;
|
|
128
|
+
private bootstrapMemoryCache;
|
|
106
129
|
private sessionPromise;
|
|
107
130
|
private locationPromise;
|
|
108
131
|
private customParams;
|
|
@@ -133,6 +156,21 @@ declare class CallForge {
|
|
|
133
156
|
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
134
157
|
*/
|
|
135
158
|
createCallIntent(): Promise<CallIntentResponse>;
|
|
159
|
+
/**
|
|
160
|
+
* Create a short-lived realtime call-link intent for a specific dialed number.
|
|
161
|
+
* Use this immediately before opening a `tel:` link.
|
|
162
|
+
*/
|
|
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>;
|
|
136
174
|
/**
|
|
137
175
|
* Subscribe to session ready event.
|
|
138
176
|
* Callback is called once session data is available.
|
|
@@ -174,7 +212,10 @@ declare class CallForge {
|
|
|
174
212
|
private getLocationId;
|
|
175
213
|
private getAutoParams;
|
|
176
214
|
private fetchFromApi;
|
|
215
|
+
private getBootstrapToken;
|
|
177
216
|
private fetchLocationFromApi;
|
|
217
|
+
private getCachedBootstrapToken;
|
|
218
|
+
private saveBootstrapToken;
|
|
178
219
|
private saveToCache;
|
|
179
220
|
private saveLocationToCache;
|
|
180
221
|
private toTrackingLocation;
|
|
@@ -183,6 +224,7 @@ declare class CallForge {
|
|
|
183
224
|
private formatApiResponse;
|
|
184
225
|
private syncParamsToCallForgeIfPossible;
|
|
185
226
|
private buildUrl;
|
|
227
|
+
private buildBootstrapUrl;
|
|
186
228
|
private buildLocationUrl;
|
|
187
229
|
}
|
|
188
230
|
|
|
@@ -192,4 +234,4 @@ declare class CallForge {
|
|
|
192
234
|
*/
|
|
193
235
|
declare function getPreloadSnippet(config: CallForgeConfig): string;
|
|
194
236
|
|
|
195
|
-
export { CallForge, type CallForgeConfig, type CallIntentResponse, type LocationReadyCallback, type ReadyCallback, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defProps = Object.defineProperties;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
7
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
@@ -17,6 +19,7 @@ var __spreadValues = (a, b) => {
|
|
|
17
19
|
}
|
|
18
20
|
return a;
|
|
19
21
|
};
|
|
22
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
23
|
var __export = (target, all) => {
|
|
21
24
|
for (var name in all)
|
|
22
25
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -203,9 +206,14 @@ var LocationCache = class {
|
|
|
203
206
|
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
204
207
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
205
208
|
var CALL_INTENT_TIMEOUT_MS = 8e3;
|
|
209
|
+
var REALTIME_CALL_LINK_TIMEOUT_MS = 5e3;
|
|
210
|
+
var REALTIME_PROFILE_TIMEOUT_MS = 5e3;
|
|
211
|
+
var BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS = 1e4;
|
|
206
212
|
var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
|
|
213
|
+
var ZIP_CODE_PATTERN = /^\d{5}$/;
|
|
207
214
|
var CallForge = class _CallForge {
|
|
208
215
|
constructor(config) {
|
|
216
|
+
this.bootstrapMemoryCache = null;
|
|
209
217
|
this.sessionPromise = null;
|
|
210
218
|
this.locationPromise = null;
|
|
211
219
|
this.customParams = {};
|
|
@@ -215,8 +223,10 @@ var CallForge = class _CallForge {
|
|
|
215
223
|
ga4MeasurementId: config.ga4MeasurementId,
|
|
216
224
|
siteKey: config.siteKey
|
|
217
225
|
};
|
|
226
|
+
const resolvedSiteKey = config.siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
|
|
218
227
|
this.cache = new TrackingCache(config.categoryId, config.siteKey);
|
|
219
228
|
this.locationCache = new LocationCache(config.siteKey);
|
|
229
|
+
this.bootstrapCacheKey = `cf_bootstrap_v1_${resolvedSiteKey}_${config.categoryId}`;
|
|
220
230
|
this.captureGA4ClientId();
|
|
221
231
|
this.startGA4ClientIdPolling();
|
|
222
232
|
}
|
|
@@ -285,6 +295,77 @@ var CallForge = class _CallForge {
|
|
|
285
295
|
clearTimeout(timeoutId);
|
|
286
296
|
}
|
|
287
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Create a short-lived realtime call-link intent for a specific dialed number.
|
|
300
|
+
* Use this immediately before opening a `tel:` link.
|
|
301
|
+
*/
|
|
302
|
+
async linkPhoneCall(input) {
|
|
303
|
+
const session = await this.getSession();
|
|
304
|
+
const controller = new AbortController();
|
|
305
|
+
const timeoutId = setTimeout(() => controller.abort(), REALTIME_CALL_LINK_TIMEOUT_MS);
|
|
306
|
+
try {
|
|
307
|
+
const response = await fetch(`${this.config.endpoint}/v1/tracking/call-link-intent`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: {
|
|
310
|
+
"Content-Type": "application/json"
|
|
311
|
+
},
|
|
312
|
+
credentials: "omit",
|
|
313
|
+
signal: controller.signal,
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
sessionToken: session.sessionToken,
|
|
316
|
+
phoneNumber: input.phoneNumber,
|
|
317
|
+
ttlSeconds: input.ttlSeconds
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
throw new Error(`Realtime call-link API error: ${response.status} ${response.statusText}`);
|
|
322
|
+
}
|
|
323
|
+
return await response.json();
|
|
324
|
+
} finally {
|
|
325
|
+
clearTimeout(timeoutId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Write visitor-selected web ZIP to the realtime data layer.
|
|
330
|
+
*/
|
|
331
|
+
async setWebZip(zipCode, options) {
|
|
332
|
+
const normalizedZip = zipCode.trim();
|
|
333
|
+
if (!ZIP_CODE_PATTERN.test(normalizedZip)) {
|
|
334
|
+
throw new Error("Invalid ZIP code. Expected a 5-digit ZIP.");
|
|
335
|
+
}
|
|
336
|
+
const session = await this.getSession();
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const timeoutId = setTimeout(() => controller.abort(), REALTIME_PROFILE_TIMEOUT_MS);
|
|
339
|
+
try {
|
|
340
|
+
const response = await fetch(`${this.config.endpoint}/v1/tracking/realtime-profile`, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers: {
|
|
343
|
+
"Content-Type": "application/json"
|
|
344
|
+
},
|
|
345
|
+
credentials: "omit",
|
|
346
|
+
signal: controller.signal,
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
sessionToken: session.sessionToken,
|
|
349
|
+
webZip: normalizedZip,
|
|
350
|
+
source: options == null ? void 0 : options.source,
|
|
351
|
+
ttlSeconds: options == null ? void 0 : options.ttlSeconds
|
|
352
|
+
})
|
|
353
|
+
});
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
throw new Error(`Realtime profile API error: ${response.status} ${response.statusText}`);
|
|
356
|
+
}
|
|
357
|
+
const result = await response.json();
|
|
358
|
+
if (result.status === "ready") {
|
|
359
|
+
this.customParams = __spreadProps(__spreadValues({}, this.customParams), {
|
|
360
|
+
webZip: result.profile.webZipCode,
|
|
361
|
+
webZipSource: result.profile.webZipSource
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return result;
|
|
365
|
+
} finally {
|
|
366
|
+
clearTimeout(timeoutId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
288
369
|
/**
|
|
289
370
|
* Subscribe to session ready event.
|
|
290
371
|
* Callback is called once session data is available.
|
|
@@ -464,7 +545,43 @@ var CallForge = class _CallForge {
|
|
|
464
545
|
return params;
|
|
465
546
|
}
|
|
466
547
|
async fetchFromApi(locationId, sessionToken, params) {
|
|
467
|
-
const
|
|
548
|
+
const controller = new AbortController();
|
|
549
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
550
|
+
try {
|
|
551
|
+
let bootstrapToken = this.getCachedBootstrapToken();
|
|
552
|
+
let response = await fetch(
|
|
553
|
+
this.buildUrl(locationId, sessionToken, params, bootstrapToken),
|
|
554
|
+
{
|
|
555
|
+
credentials: "omit",
|
|
556
|
+
signal: controller.signal
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
if (response.status === 401) {
|
|
560
|
+
bootstrapToken = await this.getBootstrapToken(true);
|
|
561
|
+
if (bootstrapToken) {
|
|
562
|
+
response = await fetch(
|
|
563
|
+
this.buildUrl(locationId, sessionToken, params, bootstrapToken),
|
|
564
|
+
{
|
|
565
|
+
credentials: "omit",
|
|
566
|
+
signal: controller.signal
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (!response.ok) {
|
|
572
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
573
|
+
}
|
|
574
|
+
return await response.json();
|
|
575
|
+
} finally {
|
|
576
|
+
clearTimeout(timeoutId);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async getBootstrapToken(forceRefresh = false) {
|
|
580
|
+
const cached = this.getCachedBootstrapToken();
|
|
581
|
+
if (!forceRefresh && cached) {
|
|
582
|
+
return cached;
|
|
583
|
+
}
|
|
584
|
+
const url = this.buildBootstrapUrl();
|
|
468
585
|
const controller = new AbortController();
|
|
469
586
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
470
587
|
try {
|
|
@@ -473,9 +590,14 @@ var CallForge = class _CallForge {
|
|
|
473
590
|
signal: controller.signal
|
|
474
591
|
});
|
|
475
592
|
if (!response.ok) {
|
|
476
|
-
|
|
593
|
+
return null;
|
|
477
594
|
}
|
|
478
|
-
|
|
595
|
+
const data = await response.json();
|
|
596
|
+
if (typeof data.bootstrapToken !== "string" || typeof data.expiresAt !== "number") {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
this.saveBootstrapToken(data.bootstrapToken, data.expiresAt);
|
|
600
|
+
return data.bootstrapToken;
|
|
479
601
|
} finally {
|
|
480
602
|
clearTimeout(timeoutId);
|
|
481
603
|
}
|
|
@@ -497,6 +619,40 @@ var CallForge = class _CallForge {
|
|
|
497
619
|
clearTimeout(timeoutId);
|
|
498
620
|
}
|
|
499
621
|
}
|
|
622
|
+
getCachedBootstrapToken() {
|
|
623
|
+
var _a;
|
|
624
|
+
const now = Date.now();
|
|
625
|
+
const fromMemory = this.bootstrapMemoryCache;
|
|
626
|
+
if (fromMemory && fromMemory.expiresAt - BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS > now) {
|
|
627
|
+
return fromMemory.token;
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
const raw = localStorage.getItem(this.bootstrapCacheKey);
|
|
631
|
+
if (!raw) return null;
|
|
632
|
+
const cached = JSON.parse(raw);
|
|
633
|
+
if (typeof cached.token !== "string" || typeof cached.expiresAt !== "number") {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
if (cached.expiresAt - BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS <= now) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
return cached.token;
|
|
640
|
+
} catch (e) {
|
|
641
|
+
return (_a = fromMemory == null ? void 0 : fromMemory.token) != null ? _a : null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
saveBootstrapToken(token, expiresAt) {
|
|
645
|
+
const cached = {
|
|
646
|
+
token,
|
|
647
|
+
expiresAt,
|
|
648
|
+
tokenVersion: "b1"
|
|
649
|
+
};
|
|
650
|
+
this.bootstrapMemoryCache = cached;
|
|
651
|
+
try {
|
|
652
|
+
localStorage.setItem(this.bootstrapCacheKey, JSON.stringify(cached));
|
|
653
|
+
} catch (e) {
|
|
654
|
+
}
|
|
655
|
+
}
|
|
500
656
|
saveToCache(locationId, data, params) {
|
|
501
657
|
const cached = {
|
|
502
658
|
locId: locationId != null ? locationId : null,
|
|
@@ -562,7 +718,7 @@ var CallForge = class _CallForge {
|
|
|
562
718
|
const data = await this.fetchFromApi(locationId, sessionToken, params);
|
|
563
719
|
this.saveToCache(locationId, data, params);
|
|
564
720
|
}
|
|
565
|
-
buildUrl(locationId, sessionToken, params) {
|
|
721
|
+
buildUrl(locationId, sessionToken, params, bootstrapToken) {
|
|
566
722
|
const { categoryId, endpoint } = this.config;
|
|
567
723
|
const queryParams = {
|
|
568
724
|
categoryId
|
|
@@ -573,6 +729,9 @@ var CallForge = class _CallForge {
|
|
|
573
729
|
if (sessionToken) {
|
|
574
730
|
queryParams.sessionToken = sessionToken;
|
|
575
731
|
}
|
|
732
|
+
if (bootstrapToken) {
|
|
733
|
+
queryParams.bootstrapToken = bootstrapToken;
|
|
734
|
+
}
|
|
576
735
|
for (const [key, value] of Object.entries(params)) {
|
|
577
736
|
if (value !== void 0) {
|
|
578
737
|
queryParams[key] = value;
|
|
@@ -582,6 +741,10 @@ var CallForge = class _CallForge {
|
|
|
582
741
|
const qs = sorted.map((k) => `${k}=${encodeURIComponent(queryParams[k])}`).join("&");
|
|
583
742
|
return `${endpoint}/v1/tracking/session?${qs}`;
|
|
584
743
|
}
|
|
744
|
+
buildBootstrapUrl() {
|
|
745
|
+
const { endpoint, categoryId } = this.config;
|
|
746
|
+
return `${endpoint}/v1/tracking/bootstrap?categoryId=${encodeURIComponent(categoryId)}`;
|
|
747
|
+
}
|
|
585
748
|
buildLocationUrl(locationId) {
|
|
586
749
|
const { endpoint } = this.config;
|
|
587
750
|
if (!locationId) {
|
|
@@ -619,6 +782,7 @@ for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
|
619
782
|
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
620
783
|
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
621
784
|
var lkey='cf_location_v1_'+site;
|
|
785
|
+
var bkey='cf_bootstrap_v1_'+site+'_${categoryId}';
|
|
622
786
|
try{
|
|
623
787
|
var cl=JSON.parse(localStorage.getItem(lkey));
|
|
624
788
|
if(cl&&cl.expiresAt>Date.now()+30000){
|
|
@@ -638,12 +802,18 @@ token=(!loc||c.locId===loc)?c.sessionToken:null;
|
|
|
638
802
|
var cp=c.params||{};
|
|
639
803
|
p=Object.assign({},cp,p);
|
|
640
804
|
}}catch(e){}
|
|
805
|
+
var bt=null;
|
|
806
|
+
try{
|
|
807
|
+
var cb=JSON.parse(localStorage.getItem(bkey));
|
|
808
|
+
if(cb&&typeof cb.token==='string'&&cb.expiresAt>Date.now()+10000)bt=cb.token;
|
|
809
|
+
}catch(e){}
|
|
810
|
+
var bp=bt?Promise.resolve({bootstrapToken:bt}):fetch('${endpoint}/v1/tracking/bootstrap?categoryId=${categoryId}',{credentials:'omit'}).then(function(r){if(!r.ok)return null;return r.json()}).then(function(b){if(!b||typeof b.bootstrapToken!=='string'||typeof b.expiresAt!=='number')return null;try{localStorage.setItem(bkey,JSON.stringify({token:b.bootstrapToken,expiresAt:b.expiresAt,tokenVersion:'b1'}))}catch(e){}return b}).catch(function(){return null});
|
|
641
811
|
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
|
|
642
812
|
if(loc)url+='&loc_physical_ms='+loc;
|
|
643
813
|
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
644
814
|
var ks=Object.keys(p).sort();
|
|
645
815
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
646
|
-
window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
816
|
+
window.__cfTracking=bp.then(function(b){if(b&&b.bootstrapToken)url+='&bootstrapToken='+encodeURIComponent(b.bootstrapToken);return fetch(url,{credentials:'omit'})}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
647
817
|
})();`.replace(/\n/g, "");
|
|
648
818
|
return `<link rel="preconnect" href="${endpoint}">
|
|
649
819
|
<script>${script}</script>`;
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
2
4
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
6
|
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
@@ -14,6 +16,7 @@ var __spreadValues = (a, b) => {
|
|
|
14
16
|
}
|
|
15
17
|
return a;
|
|
16
18
|
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
17
20
|
|
|
18
21
|
// src/cache.ts
|
|
19
22
|
var EXPIRY_BUFFER_MS = 3e4;
|
|
@@ -179,9 +182,14 @@ var LocationCache = class {
|
|
|
179
182
|
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
180
183
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
181
184
|
var CALL_INTENT_TIMEOUT_MS = 8e3;
|
|
185
|
+
var REALTIME_CALL_LINK_TIMEOUT_MS = 5e3;
|
|
186
|
+
var REALTIME_PROFILE_TIMEOUT_MS = 5e3;
|
|
187
|
+
var BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS = 1e4;
|
|
182
188
|
var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
|
|
189
|
+
var ZIP_CODE_PATTERN = /^\d{5}$/;
|
|
183
190
|
var CallForge = class _CallForge {
|
|
184
191
|
constructor(config) {
|
|
192
|
+
this.bootstrapMemoryCache = null;
|
|
185
193
|
this.sessionPromise = null;
|
|
186
194
|
this.locationPromise = null;
|
|
187
195
|
this.customParams = {};
|
|
@@ -191,8 +199,10 @@ var CallForge = class _CallForge {
|
|
|
191
199
|
ga4MeasurementId: config.ga4MeasurementId,
|
|
192
200
|
siteKey: config.siteKey
|
|
193
201
|
};
|
|
202
|
+
const resolvedSiteKey = config.siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
|
|
194
203
|
this.cache = new TrackingCache(config.categoryId, config.siteKey);
|
|
195
204
|
this.locationCache = new LocationCache(config.siteKey);
|
|
205
|
+
this.bootstrapCacheKey = `cf_bootstrap_v1_${resolvedSiteKey}_${config.categoryId}`;
|
|
196
206
|
this.captureGA4ClientId();
|
|
197
207
|
this.startGA4ClientIdPolling();
|
|
198
208
|
}
|
|
@@ -261,6 +271,77 @@ var CallForge = class _CallForge {
|
|
|
261
271
|
clearTimeout(timeoutId);
|
|
262
272
|
}
|
|
263
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Create a short-lived realtime call-link intent for a specific dialed number.
|
|
276
|
+
* Use this immediately before opening a `tel:` link.
|
|
277
|
+
*/
|
|
278
|
+
async linkPhoneCall(input) {
|
|
279
|
+
const session = await this.getSession();
|
|
280
|
+
const controller = new AbortController();
|
|
281
|
+
const timeoutId = setTimeout(() => controller.abort(), REALTIME_CALL_LINK_TIMEOUT_MS);
|
|
282
|
+
try {
|
|
283
|
+
const response = await fetch(`${this.config.endpoint}/v1/tracking/call-link-intent`, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: {
|
|
286
|
+
"Content-Type": "application/json"
|
|
287
|
+
},
|
|
288
|
+
credentials: "omit",
|
|
289
|
+
signal: controller.signal,
|
|
290
|
+
body: JSON.stringify({
|
|
291
|
+
sessionToken: session.sessionToken,
|
|
292
|
+
phoneNumber: input.phoneNumber,
|
|
293
|
+
ttlSeconds: input.ttlSeconds
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
throw new Error(`Realtime call-link API error: ${response.status} ${response.statusText}`);
|
|
298
|
+
}
|
|
299
|
+
return await response.json();
|
|
300
|
+
} finally {
|
|
301
|
+
clearTimeout(timeoutId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Write visitor-selected web ZIP to the realtime data layer.
|
|
306
|
+
*/
|
|
307
|
+
async setWebZip(zipCode, options) {
|
|
308
|
+
const normalizedZip = zipCode.trim();
|
|
309
|
+
if (!ZIP_CODE_PATTERN.test(normalizedZip)) {
|
|
310
|
+
throw new Error("Invalid ZIP code. Expected a 5-digit ZIP.");
|
|
311
|
+
}
|
|
312
|
+
const session = await this.getSession();
|
|
313
|
+
const controller = new AbortController();
|
|
314
|
+
const timeoutId = setTimeout(() => controller.abort(), REALTIME_PROFILE_TIMEOUT_MS);
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(`${this.config.endpoint}/v1/tracking/realtime-profile`, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: {
|
|
319
|
+
"Content-Type": "application/json"
|
|
320
|
+
},
|
|
321
|
+
credentials: "omit",
|
|
322
|
+
signal: controller.signal,
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
sessionToken: session.sessionToken,
|
|
325
|
+
webZip: normalizedZip,
|
|
326
|
+
source: options == null ? void 0 : options.source,
|
|
327
|
+
ttlSeconds: options == null ? void 0 : options.ttlSeconds
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
throw new Error(`Realtime profile API error: ${response.status} ${response.statusText}`);
|
|
332
|
+
}
|
|
333
|
+
const result = await response.json();
|
|
334
|
+
if (result.status === "ready") {
|
|
335
|
+
this.customParams = __spreadProps(__spreadValues({}, this.customParams), {
|
|
336
|
+
webZip: result.profile.webZipCode,
|
|
337
|
+
webZipSource: result.profile.webZipSource
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
} finally {
|
|
342
|
+
clearTimeout(timeoutId);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
264
345
|
/**
|
|
265
346
|
* Subscribe to session ready event.
|
|
266
347
|
* Callback is called once session data is available.
|
|
@@ -440,7 +521,43 @@ var CallForge = class _CallForge {
|
|
|
440
521
|
return params;
|
|
441
522
|
}
|
|
442
523
|
async fetchFromApi(locationId, sessionToken, params) {
|
|
443
|
-
const
|
|
524
|
+
const controller = new AbortController();
|
|
525
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
526
|
+
try {
|
|
527
|
+
let bootstrapToken = this.getCachedBootstrapToken();
|
|
528
|
+
let response = await fetch(
|
|
529
|
+
this.buildUrl(locationId, sessionToken, params, bootstrapToken),
|
|
530
|
+
{
|
|
531
|
+
credentials: "omit",
|
|
532
|
+
signal: controller.signal
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
if (response.status === 401) {
|
|
536
|
+
bootstrapToken = await this.getBootstrapToken(true);
|
|
537
|
+
if (bootstrapToken) {
|
|
538
|
+
response = await fetch(
|
|
539
|
+
this.buildUrl(locationId, sessionToken, params, bootstrapToken),
|
|
540
|
+
{
|
|
541
|
+
credentials: "omit",
|
|
542
|
+
signal: controller.signal
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!response.ok) {
|
|
548
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
549
|
+
}
|
|
550
|
+
return await response.json();
|
|
551
|
+
} finally {
|
|
552
|
+
clearTimeout(timeoutId);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async getBootstrapToken(forceRefresh = false) {
|
|
556
|
+
const cached = this.getCachedBootstrapToken();
|
|
557
|
+
if (!forceRefresh && cached) {
|
|
558
|
+
return cached;
|
|
559
|
+
}
|
|
560
|
+
const url = this.buildBootstrapUrl();
|
|
444
561
|
const controller = new AbortController();
|
|
445
562
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
446
563
|
try {
|
|
@@ -449,9 +566,14 @@ var CallForge = class _CallForge {
|
|
|
449
566
|
signal: controller.signal
|
|
450
567
|
});
|
|
451
568
|
if (!response.ok) {
|
|
452
|
-
|
|
569
|
+
return null;
|
|
453
570
|
}
|
|
454
|
-
|
|
571
|
+
const data = await response.json();
|
|
572
|
+
if (typeof data.bootstrapToken !== "string" || typeof data.expiresAt !== "number") {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
this.saveBootstrapToken(data.bootstrapToken, data.expiresAt);
|
|
576
|
+
return data.bootstrapToken;
|
|
455
577
|
} finally {
|
|
456
578
|
clearTimeout(timeoutId);
|
|
457
579
|
}
|
|
@@ -473,6 +595,40 @@ var CallForge = class _CallForge {
|
|
|
473
595
|
clearTimeout(timeoutId);
|
|
474
596
|
}
|
|
475
597
|
}
|
|
598
|
+
getCachedBootstrapToken() {
|
|
599
|
+
var _a;
|
|
600
|
+
const now = Date.now();
|
|
601
|
+
const fromMemory = this.bootstrapMemoryCache;
|
|
602
|
+
if (fromMemory && fromMemory.expiresAt - BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS > now) {
|
|
603
|
+
return fromMemory.token;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const raw = localStorage.getItem(this.bootstrapCacheKey);
|
|
607
|
+
if (!raw) return null;
|
|
608
|
+
const cached = JSON.parse(raw);
|
|
609
|
+
if (typeof cached.token !== "string" || typeof cached.expiresAt !== "number") {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
if (cached.expiresAt - BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS <= now) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
return cached.token;
|
|
616
|
+
} catch (e) {
|
|
617
|
+
return (_a = fromMemory == null ? void 0 : fromMemory.token) != null ? _a : null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
saveBootstrapToken(token, expiresAt) {
|
|
621
|
+
const cached = {
|
|
622
|
+
token,
|
|
623
|
+
expiresAt,
|
|
624
|
+
tokenVersion: "b1"
|
|
625
|
+
};
|
|
626
|
+
this.bootstrapMemoryCache = cached;
|
|
627
|
+
try {
|
|
628
|
+
localStorage.setItem(this.bootstrapCacheKey, JSON.stringify(cached));
|
|
629
|
+
} catch (e) {
|
|
630
|
+
}
|
|
631
|
+
}
|
|
476
632
|
saveToCache(locationId, data, params) {
|
|
477
633
|
const cached = {
|
|
478
634
|
locId: locationId != null ? locationId : null,
|
|
@@ -538,7 +694,7 @@ var CallForge = class _CallForge {
|
|
|
538
694
|
const data = await this.fetchFromApi(locationId, sessionToken, params);
|
|
539
695
|
this.saveToCache(locationId, data, params);
|
|
540
696
|
}
|
|
541
|
-
buildUrl(locationId, sessionToken, params) {
|
|
697
|
+
buildUrl(locationId, sessionToken, params, bootstrapToken) {
|
|
542
698
|
const { categoryId, endpoint } = this.config;
|
|
543
699
|
const queryParams = {
|
|
544
700
|
categoryId
|
|
@@ -549,6 +705,9 @@ var CallForge = class _CallForge {
|
|
|
549
705
|
if (sessionToken) {
|
|
550
706
|
queryParams.sessionToken = sessionToken;
|
|
551
707
|
}
|
|
708
|
+
if (bootstrapToken) {
|
|
709
|
+
queryParams.bootstrapToken = bootstrapToken;
|
|
710
|
+
}
|
|
552
711
|
for (const [key, value] of Object.entries(params)) {
|
|
553
712
|
if (value !== void 0) {
|
|
554
713
|
queryParams[key] = value;
|
|
@@ -558,6 +717,10 @@ var CallForge = class _CallForge {
|
|
|
558
717
|
const qs = sorted.map((k) => `${k}=${encodeURIComponent(queryParams[k])}`).join("&");
|
|
559
718
|
return `${endpoint}/v1/tracking/session?${qs}`;
|
|
560
719
|
}
|
|
720
|
+
buildBootstrapUrl() {
|
|
721
|
+
const { endpoint, categoryId } = this.config;
|
|
722
|
+
return `${endpoint}/v1/tracking/bootstrap?categoryId=${encodeURIComponent(categoryId)}`;
|
|
723
|
+
}
|
|
561
724
|
buildLocationUrl(locationId) {
|
|
562
725
|
const { endpoint } = this.config;
|
|
563
726
|
if (!locationId) {
|
|
@@ -595,6 +758,7 @@ for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
|
595
758
|
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
596
759
|
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
597
760
|
var lkey='cf_location_v1_'+site;
|
|
761
|
+
var bkey='cf_bootstrap_v1_'+site+'_${categoryId}';
|
|
598
762
|
try{
|
|
599
763
|
var cl=JSON.parse(localStorage.getItem(lkey));
|
|
600
764
|
if(cl&&cl.expiresAt>Date.now()+30000){
|
|
@@ -614,12 +778,18 @@ token=(!loc||c.locId===loc)?c.sessionToken:null;
|
|
|
614
778
|
var cp=c.params||{};
|
|
615
779
|
p=Object.assign({},cp,p);
|
|
616
780
|
}}catch(e){}
|
|
781
|
+
var bt=null;
|
|
782
|
+
try{
|
|
783
|
+
var cb=JSON.parse(localStorage.getItem(bkey));
|
|
784
|
+
if(cb&&typeof cb.token==='string'&&cb.expiresAt>Date.now()+10000)bt=cb.token;
|
|
785
|
+
}catch(e){}
|
|
786
|
+
var bp=bt?Promise.resolve({bootstrapToken:bt}):fetch('${endpoint}/v1/tracking/bootstrap?categoryId=${categoryId}',{credentials:'omit'}).then(function(r){if(!r.ok)return null;return r.json()}).then(function(b){if(!b||typeof b.bootstrapToken!=='string'||typeof b.expiresAt!=='number')return null;try{localStorage.setItem(bkey,JSON.stringify({token:b.bootstrapToken,expiresAt:b.expiresAt,tokenVersion:'b1'}))}catch(e){}return b}).catch(function(){return null});
|
|
617
787
|
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
|
|
618
788
|
if(loc)url+='&loc_physical_ms='+loc;
|
|
619
789
|
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
620
790
|
var ks=Object.keys(p).sort();
|
|
621
791
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
622
|
-
window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
792
|
+
window.__cfTracking=bp.then(function(b){if(b&&b.bootstrapToken)url+='&bootstrapToken='+encodeURIComponent(b.bootstrapToken);return fetch(url,{credentials:'omit'})}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
623
793
|
})();`.replace(/\n/g, "");
|
|
624
794
|
return `<link rel="preconnect" href="${endpoint}">
|
|
625
795
|
<script>${script}</script>`;
|