@layers/client 1.4.10 → 2.0.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.
@@ -0,0 +1,1305 @@
1
+ import { FetchHttpClient, LayersCore, LayersError as LayersError$1, createDefaultPersistence } from "@layers/core-wasm";
2
+
3
+ //#region src/attribution.ts
4
+ const CLICK_ID_PARAMS = [
5
+ "fbclid",
6
+ "gclid",
7
+ "gbraid",
8
+ "wbraid",
9
+ "ttclid",
10
+ "msclkid",
11
+ "rclid"
12
+ ];
13
+ const UTM_PARAMS = [
14
+ "utm_source",
15
+ "utm_medium",
16
+ "utm_campaign",
17
+ "utm_content",
18
+ "utm_term"
19
+ ];
20
+ const STORAGE_KEY = "layers_attribution";
21
+ const TTL_MS = 720 * 60 * 60 * 1e3;
22
+ /**
23
+ * Capture attribution signals from the current page URL and referrer.
24
+ * Persists to localStorage with a 30-day TTL. Click IDs take priority:
25
+ * if a new click ID is present, the entire record is overwritten.
26
+ */
27
+ function captureAttribution() {
28
+ if (typeof window === "undefined" || typeof localStorage === "undefined") return;
29
+ let params;
30
+ try {
31
+ params = new URLSearchParams(window.location.search);
32
+ } catch {
33
+ return;
34
+ }
35
+ let clickIdParam;
36
+ let clickIdValue;
37
+ for (const param of CLICK_ID_PARAMS) {
38
+ const value = params.get(param);
39
+ if (value) {
40
+ clickIdParam = param;
41
+ clickIdValue = value;
42
+ break;
43
+ }
44
+ }
45
+ const utms = {};
46
+ let hasUtm = false;
47
+ for (const param of UTM_PARAMS) {
48
+ const value = params.get(param);
49
+ if (value) {
50
+ utms[param] = value;
51
+ hasUtm = true;
52
+ }
53
+ }
54
+ const referrer = typeof document !== "undefined" && document.referrer ? document.referrer : void 0;
55
+ if (!clickIdParam && !hasUtm && !referrer) return;
56
+ const existing = getAttribution();
57
+ if (clickIdParam) {
58
+ writeAttribution({
59
+ click_id_param: clickIdParam,
60
+ ...clickIdValue != null && { click_id_value: clickIdValue },
61
+ ...utms,
62
+ ...referrer != null && { referrer_url: referrer },
63
+ captured_at: Date.now()
64
+ });
65
+ return;
66
+ }
67
+ if (hasUtm) {
68
+ writeAttribution({
69
+ ...existing?.click_id_param != null && { click_id_param: existing.click_id_param },
70
+ ...existing?.click_id_value != null && { click_id_value: existing.click_id_value },
71
+ ...utms,
72
+ ...referrer != null && { referrer_url: referrer },
73
+ captured_at: Date.now()
74
+ });
75
+ return;
76
+ }
77
+ if (!existing) writeAttribution({
78
+ ...referrer != null && { referrer_url: referrer },
79
+ captured_at: Date.now()
80
+ });
81
+ }
82
+ /**
83
+ * Read stored attribution data, returning null if missing or expired.
84
+ */
85
+ function getAttribution() {
86
+ if (typeof localStorage === "undefined") return null;
87
+ try {
88
+ const raw = localStorage.getItem(STORAGE_KEY);
89
+ if (!raw) return null;
90
+ const data = JSON.parse(raw);
91
+ if (Date.now() - data.captured_at > TTL_MS) {
92
+ localStorage.removeItem(STORAGE_KEY);
93
+ return null;
94
+ }
95
+ return data;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ /**
101
+ * Return a flat property bag suitable for merging into event properties.
102
+ * Keys are prefixed with `$attribution_` to avoid collisions.
103
+ */
104
+ function getAttributionProperties() {
105
+ const data = getAttribution();
106
+ if (!data) return {};
107
+ const props = {};
108
+ if (data.click_id_param) props["$attribution_click_id_param"] = data.click_id_param;
109
+ if (data.click_id_value) props["$attribution_click_id_value"] = data.click_id_value;
110
+ if (data.utm_source) props["$attribution_utm_source"] = data.utm_source;
111
+ if (data.utm_medium) props["$attribution_utm_medium"] = data.utm_medium;
112
+ if (data.utm_campaign) props["$attribution_utm_campaign"] = data.utm_campaign;
113
+ if (data.utm_content) props["$attribution_utm_content"] = data.utm_content;
114
+ if (data.utm_term) props["$attribution_utm_term"] = data.utm_term;
115
+ if (data.referrer_url) props["$attribution_referrer_url"] = data.referrer_url;
116
+ return props;
117
+ }
118
+ function writeAttribution(data) {
119
+ try {
120
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
121
+ } catch {}
122
+ }
123
+
124
+ //#endregion
125
+ //#region src/capi.ts
126
+ /**
127
+ * Read a named cookie from document.cookie.
128
+ * Returns the cookie value or undefined if not found / unavailable.
129
+ */
130
+ function getCookie(name) {
131
+ if (typeof document === "undefined" || !document.cookie) return void 0;
132
+ const prefix = `${name}=`;
133
+ const cookies = document.cookie.split("; ");
134
+ for (const cookie of cookies) if (cookie.startsWith(prefix)) return decodeURIComponent(cookie.slice(prefix.length));
135
+ }
136
+ /**
137
+ * Read Meta's _fbp cookie.
138
+ * Format set by Meta Pixel: fb.1.{timestamp}.{random}
139
+ */
140
+ function getFbpCookie() {
141
+ return getCookie("_fbp");
142
+ }
143
+ /**
144
+ * Read TikTok's _ttp cookie.
145
+ */
146
+ function getTtpCookie() {
147
+ return getCookie("_ttp");
148
+ }
149
+ /**
150
+ * Capture the current page URL.
151
+ * Required by Meta CAPI for every web event (event_source_url).
152
+ */
153
+ function getPageUrl() {
154
+ if (typeof window === "undefined") return void 0;
155
+ try {
156
+ return window.location.href;
157
+ } catch {
158
+ return;
159
+ }
160
+ }
161
+ /**
162
+ * Format an fbclid value into Meta's fbc cookie format.
163
+ * Format: fb.1.{timestamp_ms}.{fbclid}
164
+ *
165
+ * @param fbclid - The raw fbclid parameter value from the URL
166
+ * @param timestampMs - Optional timestamp in milliseconds (defaults to Date.now())
167
+ */
168
+ function formatFbc(fbclid, timestampMs) {
169
+ return `fb.1.${timestampMs ?? Date.now()}.${fbclid}`;
170
+ }
171
+ /**
172
+ * Get the fbc value. Checks in order:
173
+ * 1. If an fbclid URL parameter is present, format it as fbc
174
+ * 2. If an _fbc cookie exists, return it
175
+ * 3. Return undefined
176
+ */
177
+ function getFbc() {
178
+ if (typeof window !== "undefined") try {
179
+ const fbclid = new URLSearchParams(window.location.search).get("fbclid");
180
+ if (fbclid) return formatFbc(fbclid);
181
+ } catch {}
182
+ return getCookie("_fbc");
183
+ }
184
+ /**
185
+ * Build the full set of CAPI properties for an event.
186
+ * All values are optional — only present keys are included.
187
+ */
188
+ function getCapiProperties() {
189
+ const props = {};
190
+ const fbp = getFbpCookie();
191
+ if (fbp) props["$fbp"] = fbp;
192
+ const ttp = getTtpCookie();
193
+ if (ttp) props["$ttp"] = ttp;
194
+ const pageUrl = getPageUrl();
195
+ if (pageUrl) props["$page_url"] = pageUrl;
196
+ const fbc = getFbc();
197
+ if (fbc) props["$fbc"] = fbc;
198
+ return props;
199
+ }
200
+
201
+ //#endregion
202
+ //#region src/debug-overlay.ts
203
+ const OVERLAY_ID = "layers-debug-overlay";
204
+ const OVERLAY_STYLES = `
205
+ position: fixed;
206
+ bottom: 8px;
207
+ right: 8px;
208
+ width: 360px;
209
+ max-height: 400px;
210
+ background: rgba(0, 0, 0, 0.88);
211
+ color: #e0e0e0;
212
+ font-family: monospace;
213
+ font-size: 11px;
214
+ line-height: 1.5;
215
+ padding: 12px;
216
+ border-radius: 8px;
217
+ z-index: 999999;
218
+ overflow-y: auto;
219
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
220
+ pointer-events: auto;
221
+ `;
222
+ let overlayElement = null;
223
+ let updateInterval = null;
224
+ /**
225
+ * Create and display the debug overlay in the DOM.
226
+ * Updates every second with the latest state from the provider.
227
+ */
228
+ function enableDebugOverlay(stateProvider) {
229
+ if (typeof document === "undefined") return;
230
+ if (overlayElement) disableDebugOverlay();
231
+ overlayElement = document.createElement("div");
232
+ overlayElement.id = OVERLAY_ID;
233
+ overlayElement.setAttribute("style", OVERLAY_STYLES);
234
+ document.body.appendChild(overlayElement);
235
+ const update = () => {
236
+ if (!overlayElement) return;
237
+ overlayElement.innerHTML = renderOverlay(stateProvider());
238
+ };
239
+ update();
240
+ updateInterval = setInterval(update, 1e3);
241
+ }
242
+ /**
243
+ * Remove the debug overlay from the DOM and stop updates.
244
+ */
245
+ function disableDebugOverlay() {
246
+ if (updateInterval) {
247
+ clearInterval(updateInterval);
248
+ updateInterval = null;
249
+ }
250
+ if (overlayElement && overlayElement.parentNode) overlayElement.parentNode.removeChild(overlayElement);
251
+ overlayElement = null;
252
+ }
253
+ function renderOverlay(state) {
254
+ const lines = [
255
+ `<div style="font-size:13px;font-weight:bold;margin-bottom:8px;">Layers Debug ${state.isOnline ? "&#x1F7E2;" : "&#x1F534;"}</div>`,
256
+ `<div><b>SDK:</b> ${escapeHtml(state.sdkVersion)}</div>`,
257
+ `<div><b>App:</b> ${escapeHtml(state.appId)} (${escapeHtml(state.environment)})</div>`,
258
+ `<div><b>Session:</b> ${escapeHtml(truncate(state.sessionId, 20))}</div>`,
259
+ `<div><b>Install:</b> ${escapeHtml(truncate(state.installId ?? "N/A", 20))}</div>`,
260
+ `<div><b>Queue:</b> ${String(state.queueDepth)} events</div>`,
261
+ `<div><b>Network:</b> ${state.isOnline ? "Online" : "Offline"}</div>`
262
+ ];
263
+ if (state.recentEvents.length > 0) {
264
+ lines.push(`<div style="margin-top:8px;font-weight:bold;border-top:1px solid #444;padding-top:6px;">Recent Events</div>`);
265
+ for (const event of state.recentEvents.slice(0, 10)) lines.push(`<div style="color:#aaa;">${escapeHtml(event)}</div>`);
266
+ } else lines.push(`<div style="margin-top:8px;color:#888;">No events tracked yet</div>`);
267
+ return lines.join("");
268
+ }
269
+ function escapeHtml(str) {
270
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
271
+ }
272
+ function truncate(str, maxLen) {
273
+ if (str.length <= maxLen) return str;
274
+ return `${str.slice(0, maxLen)}...`;
275
+ }
276
+
277
+ //#endregion
278
+ //#region src/deep-links.ts
279
+ /**
280
+ * Parse a URL string into structured DeepLinkData.
281
+ * Returns null if the URL cannot be parsed.
282
+ */
283
+ function parseDeepLink(url) {
284
+ try {
285
+ const parsed = new URL(url);
286
+ const params = {};
287
+ parsed.searchParams.forEach((value, key) => {
288
+ params[key] = value;
289
+ });
290
+ return {
291
+ url,
292
+ scheme: parsed.protocol.replace(":", ""),
293
+ host: parsed.hostname,
294
+ path: parsed.pathname,
295
+ queryParams: params,
296
+ timestamp: Date.now()
297
+ };
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
302
+ /**
303
+ * Set up a listener for URL changes in a web browser context.
304
+ * Listens for `popstate` and `hashchange` events and calls the callback
305
+ * with parsed deep link data on each URL change.
306
+ *
307
+ * Returns an unsubscribe function to remove the listeners.
308
+ */
309
+ function setupDeepLinkListener(onDeepLink) {
310
+ if (typeof window === "undefined" || !window.location) return () => {};
311
+ let lastUrl;
312
+ try {
313
+ lastUrl = window.location.href;
314
+ } catch {
315
+ return () => {};
316
+ }
317
+ const handleUrlChange = () => {
318
+ const currentUrl = window.location.href;
319
+ if (currentUrl === lastUrl) return;
320
+ lastUrl = currentUrl;
321
+ const parsed = parseDeepLink(currentUrl);
322
+ if (parsed) onDeepLink(parsed);
323
+ };
324
+ window.addEventListener("popstate", handleUrlChange);
325
+ window.addEventListener("hashchange", handleUrlChange);
326
+ const initialData = parseDeepLink(window.location.href);
327
+ if (initialData && Object.keys(initialData.queryParams).length > 0) onDeepLink(initialData);
328
+ return () => {
329
+ window.removeEventListener("popstate", handleUrlChange);
330
+ window.removeEventListener("hashchange", handleUrlChange);
331
+ };
332
+ }
333
+
334
+ //#endregion
335
+ //#region src/standard-events.ts
336
+ /**
337
+ * Predefined standard event name constants.
338
+ *
339
+ * Usage:
340
+ * ```ts
341
+ * import { StandardEvents } from '@layers/client';
342
+ * client.track(StandardEvents.PURCHASE, { amount: 9.99, currency: 'USD' });
343
+ * ```
344
+ */
345
+ const StandardEvents = {
346
+ APP_OPEN: "app_open",
347
+ LOGIN: "login",
348
+ SIGN_UP: "sign_up",
349
+ REGISTER: "register",
350
+ PURCHASE: "purchase_success",
351
+ ADD_TO_CART: "add_to_cart",
352
+ ADD_TO_WISHLIST: "add_to_wishlist",
353
+ INITIATE_CHECKOUT: "initiate_checkout",
354
+ BEGIN_CHECKOUT: "begin_checkout",
355
+ START_TRIAL: "start_trial",
356
+ SUBSCRIBE: "subscribe",
357
+ LEVEL_START: "level_start",
358
+ LEVEL_COMPLETE: "level_complete",
359
+ TUTORIAL_COMPLETE: "tutorial_complete",
360
+ SEARCH: "search",
361
+ VIEW_ITEM: "view_item",
362
+ VIEW_CONTENT: "view_content",
363
+ SHARE: "share",
364
+ DEEP_LINK: "deep_link_opened",
365
+ SCREEN_VIEW: "screen_view"
366
+ };
367
+ /** Build a login event. */
368
+ function loginEvent(method) {
369
+ const properties = {};
370
+ if (method !== void 0) properties.method = method;
371
+ return {
372
+ event: StandardEvents.LOGIN,
373
+ properties
374
+ };
375
+ }
376
+ /** Build a sign-up event. */
377
+ function signUpEvent(method) {
378
+ const properties = {};
379
+ if (method !== void 0) properties.method = method;
380
+ return {
381
+ event: StandardEvents.SIGN_UP,
382
+ properties
383
+ };
384
+ }
385
+ /** Build a register event. */
386
+ function registerEvent(method) {
387
+ const properties = {};
388
+ if (method !== void 0) properties.method = method;
389
+ return {
390
+ event: StandardEvents.REGISTER,
391
+ properties
392
+ };
393
+ }
394
+ /** Build a purchase event. */
395
+ function purchaseEvent(amount, currency = "USD", itemId) {
396
+ const properties = {
397
+ amount,
398
+ currency
399
+ };
400
+ if (itemId !== void 0) properties.item_id = itemId;
401
+ return {
402
+ event: StandardEvents.PURCHASE,
403
+ properties
404
+ };
405
+ }
406
+ /** Build an add-to-cart event. */
407
+ function addToCartEvent(itemId, price, quantity = 1) {
408
+ return {
409
+ event: StandardEvents.ADD_TO_CART,
410
+ properties: {
411
+ item_id: itemId,
412
+ price,
413
+ quantity
414
+ }
415
+ };
416
+ }
417
+ /** Build an add-to-wishlist event. */
418
+ function addToWishlistEvent(itemId, name, price) {
419
+ const properties = { item_id: itemId };
420
+ if (name !== void 0) properties.name = name;
421
+ if (price !== void 0) properties.price = price;
422
+ return {
423
+ event: StandardEvents.ADD_TO_WISHLIST,
424
+ properties
425
+ };
426
+ }
427
+ /** Build an initiate-checkout event. */
428
+ function initiateCheckoutEvent(value, currency = "USD", itemCount) {
429
+ const properties = {
430
+ value,
431
+ currency
432
+ };
433
+ if (itemCount !== void 0) properties.item_count = itemCount;
434
+ return {
435
+ event: StandardEvents.INITIATE_CHECKOUT,
436
+ properties
437
+ };
438
+ }
439
+ /** Build a start-trial event. */
440
+ function startTrialEvent(plan, durationDays) {
441
+ const properties = {};
442
+ if (plan !== void 0) properties.plan = plan;
443
+ if (durationDays !== void 0) properties.duration_days = durationDays;
444
+ return {
445
+ event: StandardEvents.START_TRIAL,
446
+ properties
447
+ };
448
+ }
449
+ /** Build a subscribe event. */
450
+ function subscribeEvent(plan, amount, currency = "USD") {
451
+ return {
452
+ event: StandardEvents.SUBSCRIBE,
453
+ properties: {
454
+ plan,
455
+ amount,
456
+ currency
457
+ }
458
+ };
459
+ }
460
+ /** Build a level-start event. */
461
+ function levelStartEvent(level) {
462
+ return {
463
+ event: StandardEvents.LEVEL_START,
464
+ properties: { level }
465
+ };
466
+ }
467
+ /** Build a level-complete event. */
468
+ function levelCompleteEvent(level, score) {
469
+ const properties = { level };
470
+ if (score !== void 0) properties.score = score;
471
+ return {
472
+ event: StandardEvents.LEVEL_COMPLETE,
473
+ properties
474
+ };
475
+ }
476
+ /** Build a tutorial-complete event. */
477
+ function tutorialCompleteEvent(name) {
478
+ const properties = {};
479
+ if (name !== void 0) properties.name = name;
480
+ return {
481
+ event: StandardEvents.TUTORIAL_COMPLETE,
482
+ properties
483
+ };
484
+ }
485
+ /** Build a search event. */
486
+ function searchEvent(query, resultCount) {
487
+ const properties = { query };
488
+ if (resultCount !== void 0) properties.result_count = resultCount;
489
+ return {
490
+ event: StandardEvents.SEARCH,
491
+ properties
492
+ };
493
+ }
494
+ /** Build a view-item event. */
495
+ function viewItemEvent(itemId, name, category) {
496
+ const properties = { item_id: itemId };
497
+ if (name !== void 0) properties.name = name;
498
+ if (category !== void 0) properties.category = category;
499
+ return {
500
+ event: StandardEvents.VIEW_ITEM,
501
+ properties
502
+ };
503
+ }
504
+ /** Build a view-content event. */
505
+ function viewContentEvent(contentId, contentType, name) {
506
+ const properties = { content_id: contentId };
507
+ if (contentType !== void 0) properties.content_type = contentType;
508
+ if (name !== void 0) properties.name = name;
509
+ return {
510
+ event: StandardEvents.VIEW_CONTENT,
511
+ properties
512
+ };
513
+ }
514
+ /** Build a share event. */
515
+ function shareEvent(contentType, method, contentId) {
516
+ const properties = { content_type: contentType };
517
+ if (method !== void 0) properties.method = method;
518
+ if (contentId !== void 0) properties.content_id = contentId;
519
+ return {
520
+ event: StandardEvents.SHARE,
521
+ properties
522
+ };
523
+ }
524
+ /** Build a screen-view event. */
525
+ function screenViewEvent(name, screenClass) {
526
+ const properties = { screen_name: name };
527
+ if (screenClass !== void 0) properties.screen_class = screenClass;
528
+ return {
529
+ event: StandardEvents.SCREEN_VIEW,
530
+ properties
531
+ };
532
+ }
533
+
534
+ //#endregion
535
+ //#region src/commerce.ts
536
+ /**
537
+ * Track a successful purchase.
538
+ *
539
+ * ```ts
540
+ * import { trackPurchase } from '@layers/client';
541
+ *
542
+ * trackPurchase(layers, {
543
+ * productId: 'premium_monthly',
544
+ * price: 9.99,
545
+ * currency: 'USD',
546
+ * transactionId: 'txn_abc123',
547
+ * });
548
+ * ```
549
+ */
550
+ function trackPurchase(layers, params) {
551
+ const quantity = params.quantity ?? 1;
552
+ const props = {
553
+ product_id: params.productId,
554
+ price: params.price,
555
+ currency: params.currency,
556
+ quantity,
557
+ revenue: params.price * quantity,
558
+ ...params.properties
559
+ };
560
+ if (params.transactionId !== void 0) props.transaction_id = params.transactionId;
561
+ if (params.isRestored !== void 0) props.is_restored = params.isRestored;
562
+ if (params.store !== void 0) props.store = params.store;
563
+ layers.track("purchase_success", props);
564
+ }
565
+ /**
566
+ * Track a failed purchase attempt.
567
+ */
568
+ function trackPurchaseFailed(layers, params) {
569
+ const props = {
570
+ product_id: params.productId,
571
+ currency: params.currency,
572
+ error_code: params.errorCode,
573
+ ...params.properties
574
+ };
575
+ if (params.errorMessage !== void 0) props.error_message = params.errorMessage;
576
+ layers.track("purchase_failed", props);
577
+ }
578
+ /**
579
+ * Track a subscription purchase or renewal.
580
+ *
581
+ * ```ts
582
+ * import { trackSubscription } from '@layers/client';
583
+ *
584
+ * trackSubscription(layers, {
585
+ * productId: 'pro_annual',
586
+ * price: 49.99,
587
+ * currency: 'USD',
588
+ * period: 'P1Y',
589
+ * });
590
+ * ```
591
+ */
592
+ function trackSubscription(layers, params) {
593
+ const props = {
594
+ product_id: params.productId,
595
+ price: params.price,
596
+ currency: params.currency,
597
+ quantity: 1,
598
+ revenue: params.price,
599
+ ...params.properties
600
+ };
601
+ if (params.transactionId !== void 0) props.transaction_id = params.transactionId;
602
+ if (params.period !== void 0) props.period = params.period;
603
+ if (params.isRenewal !== void 0) props.is_renewal = params.isRenewal;
604
+ if (params.isTrial !== void 0) props.is_trial = params.isTrial;
605
+ if (params.subscriptionGroupId !== void 0) props.subscription_group_id = params.subscriptionGroupId;
606
+ if (params.originalTransactionId !== void 0) props.original_transaction_id = params.originalTransactionId;
607
+ layers.track("subscribe", props);
608
+ }
609
+ /**
610
+ * Track a completed order with multiple line items.
611
+ */
612
+ function trackOrder(layers, params) {
613
+ const currency = params.currency ?? "USD";
614
+ let total = params.subtotal;
615
+ if (params.tax !== void 0) total += params.tax;
616
+ if (params.shipping !== void 0) total += params.shipping;
617
+ if (params.discount !== void 0) total -= params.discount;
618
+ const props = {
619
+ order_id: params.orderId,
620
+ subtotal: params.subtotal,
621
+ total,
622
+ currency,
623
+ item_count: params.items.length,
624
+ revenue: total,
625
+ product_ids: params.items.map((i) => i.productId).join(","),
626
+ ...params.properties
627
+ };
628
+ if (params.tax !== void 0) props.tax = params.tax;
629
+ if (params.shipping !== void 0) props.shipping = params.shipping;
630
+ if (params.discount !== void 0) props.discount = params.discount;
631
+ if (params.couponCode !== void 0) props.coupon_code = params.couponCode;
632
+ layers.track("purchase_success", props);
633
+ }
634
+ /**
635
+ * Track an item being added to the cart.
636
+ */
637
+ function trackAddToCart(layers, item, properties) {
638
+ const quantity = item.quantity ?? 1;
639
+ const props = {
640
+ product_id: item.productId,
641
+ product_name: item.name,
642
+ price: item.price,
643
+ quantity,
644
+ value: item.price * quantity,
645
+ ...properties
646
+ };
647
+ if (item.category !== void 0) props.category = item.category;
648
+ layers.track("add_to_cart", props);
649
+ }
650
+ /**
651
+ * Track an item being removed from the cart.
652
+ */
653
+ function trackRemoveFromCart(layers, item, properties) {
654
+ const quantity = item.quantity ?? 1;
655
+ const props = {
656
+ product_id: item.productId,
657
+ product_name: item.name,
658
+ price: item.price,
659
+ quantity,
660
+ ...properties
661
+ };
662
+ if (item.category !== void 0) props.category = item.category;
663
+ layers.track("remove_from_cart", props);
664
+ }
665
+ /**
666
+ * Track beginning the checkout flow.
667
+ */
668
+ function trackBeginCheckout(layers, items, currency = "USD", properties) {
669
+ const total = items.reduce((sum, item) => sum + item.price * (item.quantity ?? 1), 0);
670
+ const props = {
671
+ item_count: items.length,
672
+ value: total,
673
+ currency,
674
+ product_ids: items.map((i) => i.productId).join(","),
675
+ ...properties
676
+ };
677
+ layers.track("begin_checkout", props);
678
+ }
679
+ /**
680
+ * Track viewing a product detail page.
681
+ */
682
+ function trackViewProduct(layers, productId, name, price, currency = "USD", category, properties) {
683
+ const props = {
684
+ product_id: productId,
685
+ product_name: name,
686
+ price,
687
+ currency,
688
+ ...properties
689
+ };
690
+ if (category !== void 0) props.category = category;
691
+ layers.track("view_item", props);
692
+ }
693
+ /**
694
+ * Track a refund.
695
+ */
696
+ function trackRefund(layers, params) {
697
+ const props = {
698
+ transaction_id: params.transactionId,
699
+ amount: params.amount,
700
+ currency: params.currency,
701
+ ...params.properties
702
+ };
703
+ if (params.reason !== void 0) props.reason = params.reason;
704
+ layers.track("refund", props);
705
+ }
706
+
707
+ //#endregion
708
+ //#region src/index.ts
709
+ const INSTALL_ID_KEY = "layers_install_id";
710
+ const FIRST_LAUNCH_TRACKED_KEY = "layers_first_launch_tracked";
711
+ const FIRST_INSTALL_TIME_KEY = "layers_first_install_time";
712
+ const CLIPBOARD_CHECKED_KEY = "layers_clipboard_checked";
713
+ const ATTRIBUTION_DATA_KEY = "layers_attribution_data";
714
+ /** Maximum age of an installation for install event gating (24 hours). */
715
+ const INSTALL_EVENT_MAX_DIFF_MS = 1440 * 60 * 1e3;
716
+ var LayersClient = class LayersClient {
717
+ core;
718
+ appUserId;
719
+ isOnline = true;
720
+ enableDebug;
721
+ baseUrl;
722
+ config;
723
+ onlineListener = null;
724
+ offlineListener = null;
725
+ visibilityListener = null;
726
+ beforeUnloadListener = null;
727
+ deepLinkUnsubscribe = null;
728
+ errorListeners = /* @__PURE__ */ new Set();
729
+ _installId = null;
730
+ _hadPriorSdkState = false;
731
+ _attributionData = null;
732
+ static MAX_RECENT_EVENTS = 20;
733
+ _recentEvents = [];
734
+ _isInitialized = false;
735
+ _initListener = null;
736
+ _debugOverlayActive = false;
737
+ constructor(config) {
738
+ this.enableDebug = config.enableDebug ?? false;
739
+ this.baseUrl = (config.baseUrl ?? "https://in.layers.com").replace(/\/$/, "");
740
+ this.appUserId = config.appUserId;
741
+ this.config = config;
742
+ const persistence = createDefaultPersistence(config.appId);
743
+ const httpClient = new FetchHttpClient();
744
+ this.core = LayersCore.init({
745
+ config: {
746
+ appId: config.appId,
747
+ environment: config.environment ?? "production",
748
+ ...config.baseUrl != null && { baseUrl: config.baseUrl },
749
+ ...config.enableDebug != null && { enableDebug: config.enableDebug },
750
+ ...config.flushIntervalMs != null && { flushIntervalMs: config.flushIntervalMs },
751
+ ...config.flushThreshold != null && { flushThreshold: config.flushThreshold },
752
+ ...config.maxQueueSize != null && { maxQueueSize: config.maxQueueSize },
753
+ sdkVersion: `client/${SDK_VERSION}`
754
+ },
755
+ httpClient,
756
+ persistence
757
+ });
758
+ if (this.appUserId) this.core.identify(this.appUserId);
759
+ }
760
+ /** Initialize the client: detects device info, attaches lifecycle listeners, and fetches remote config. */
761
+ async init() {
762
+ const initStartTime = typeof performance !== "undefined" ? performance.now() : Date.now();
763
+ this.initializeInstallId();
764
+ this.initializeDeviceInfo();
765
+ this.setupNetworkListener();
766
+ this.setupLifecycleListeners();
767
+ captureAttribution();
768
+ this.restoreAttributionData();
769
+ const mainThreadEndTime = typeof performance !== "undefined" ? performance.now() : Date.now();
770
+ const mainThreadDurationMs = Math.round(mainThreadEndTime - initStartTime);
771
+ await this.core.fetchRemoteConfig().catch(() => {});
772
+ this.checkClipboardAttribution();
773
+ if (this.config.autoTrackAppOpen !== false) {
774
+ const appOpenProps = {};
775
+ const isFirstLaunch = this.shouldTreatAsNewInstall();
776
+ appOpenProps.is_first_launch = isFirstLaunch;
777
+ if (isFirstLaunch) this.storageSet(FIRST_LAUNCH_TRACKED_KEY, "true");
778
+ this.track("app_open", appOpenProps);
779
+ }
780
+ if (this.config.autoTrackDeepLinks !== false) this.setupDeepLinkAutoTracking();
781
+ this._isInitialized = true;
782
+ const totalEndTime = typeof performance !== "undefined" ? performance.now() : Date.now();
783
+ const totalDurationMs = Math.round(totalEndTime - initStartTime);
784
+ this.trackInitTiming(mainThreadDurationMs, totalDurationMs);
785
+ if (this.enableDebug) console.log(`[Layers] Init timing: mainThread=${String(mainThreadDurationMs)}ms, total=${String(totalDurationMs)}ms`);
786
+ if (this._initListener) try {
787
+ this._initListener(mainThreadDurationMs, totalDurationMs);
788
+ } catch (e) {
789
+ if (this.enableDebug) console.warn("[Layers] InitListener threw:", e);
790
+ }
791
+ }
792
+ /**
793
+ * Record a custom analytics event with an optional property bag.
794
+ *
795
+ * Events are batched and flushed automatically when the queue reaches
796
+ * `flushThreshold` or the periodic flush timer fires.
797
+ */
798
+ track(eventName, properties) {
799
+ if (this.enableDebug) console.log(`[Layers] track("${eventName}", ${Object.keys(properties ?? {}).length} properties)`);
800
+ try {
801
+ const merged = this.mergeAllProperties(properties);
802
+ const depthBefore = this.core.queueDepth();
803
+ this.core.track(eventName, merged, this.appUserId);
804
+ const depthAfter = this.core.queueDepth();
805
+ if (depthAfter <= depthBefore && this.enableDebug) console.warn(`[Layers] track("${eventName}") — event may not have been queued (depth before=${String(depthBefore)}, after=${String(depthAfter)})`);
806
+ this.recordRecentEvent(eventName, properties);
807
+ } catch (e) {
808
+ this.emitError(e);
809
+ }
810
+ }
811
+ /**
812
+ * Record a screen view event with an optional property bag.
813
+ *
814
+ * Events are batched and flushed automatically when the queue reaches
815
+ * `flushThreshold` or the periodic flush timer fires.
816
+ */
817
+ screen(screenName, properties) {
818
+ if (this.enableDebug) console.log(`[Layers] screen("${screenName}", ${Object.keys(properties ?? {}).length} properties)`);
819
+ try {
820
+ const merged = this.mergeAllProperties(properties);
821
+ const depthBefore = this.core.queueDepth();
822
+ this.core.screen(screenName, merged, this.appUserId);
823
+ const depthAfter = this.core.queueDepth();
824
+ if (depthAfter <= depthBefore && this.enableDebug) console.warn(`[Layers] screen("${screenName}") — event may not have been queued (depth before=${String(depthBefore)}, after=${String(depthAfter)})`);
825
+ this.recordRecentEvent(`screen:${screenName}`, properties);
826
+ } catch (e) {
827
+ this.emitError(e);
828
+ }
829
+ }
830
+ /** Set or update user-level properties that persist across sessions. */
831
+ setUserProperties(properties) {
832
+ this.core.setUserProperties(properties);
833
+ }
834
+ /** Set user-level properties with "set once" semantics — only keys not previously set are applied. */
835
+ setUserPropertiesOnce(properties) {
836
+ this.core.setUserPropertiesOnce(properties);
837
+ }
838
+ /** Update the user's consent state for analytics and advertising data collection. */
839
+ setConsent(consent) {
840
+ this.core.setConsent(consent);
841
+ }
842
+ /**
843
+ * Associate all subsequent events with a group (company, team, organization).
844
+ * Pass `undefined` or empty string to clear the group association.
845
+ */
846
+ group(groupId, properties) {
847
+ if (this.enableDebug) console.log(`[Layers] group(${groupId ? `"${groupId}"` : "undefined"}, ${Object.keys(properties ?? {}).length} properties)`);
848
+ try {
849
+ this.core.group(groupId ?? "", properties);
850
+ } catch (e) {
851
+ this.emitError(e);
852
+ }
853
+ }
854
+ /** Associate all subsequent events with the given user ID, or clear it with `undefined`. */
855
+ setAppUserId(appUserId) {
856
+ if (this.enableDebug) console.log(`[Layers] setAppUserId(${appUserId ? `"${appUserId}"` : "undefined"})`);
857
+ this.appUserId = appUserId;
858
+ if (appUserId) this.core.identify(appUserId);
859
+ else this.core.identify("");
860
+ }
861
+ /** @deprecated Use setAppUserId instead */
862
+ setUserId(userId) {
863
+ this.setAppUserId(userId);
864
+ }
865
+ /** Return the current app user ID, or `undefined` if not set. */
866
+ getAppUserId() {
867
+ return this.appUserId;
868
+ }
869
+ /** @deprecated Use getAppUserId instead */
870
+ getUserId() {
871
+ return this.appUserId;
872
+ }
873
+ /** Return the current anonymous session ID. */
874
+ getSessionId() {
875
+ return this.core.getSessionId();
876
+ }
877
+ /** Return the current consent state for analytics and advertising. */
878
+ getConsentState() {
879
+ return this.core.getConsentState();
880
+ }
881
+ /** Override device-level context fields (platform, OS, locale, etc.). */
882
+ setDeviceInfo(deviceInfo) {
883
+ this.core.setDeviceContext(deviceInfo);
884
+ }
885
+ /**
886
+ * Return the install ID (persistent UUID stored in localStorage).
887
+ * Generated on first init, persists across sessions.
888
+ */
889
+ getInstallId() {
890
+ return this._installId;
891
+ }
892
+ /**
893
+ * Return the number of events currently queued.
894
+ */
895
+ getQueueDepth() {
896
+ return this.core.queueDepth();
897
+ }
898
+ /**
899
+ * Store attribution data that will be attached to all subsequent events.
900
+ *
901
+ * The values are persisted in localStorage so they survive page reloads.
902
+ * Pass `null` to clear a value.
903
+ *
904
+ * When set, `deeplinkId`, `gclid`, `fbclid`, `ttclid`, and/or `msclkid`
905
+ * are included in every event's properties.
906
+ */
907
+ setAttributionData(data) {
908
+ this._attributionData = data;
909
+ try {
910
+ const ctx = { ...this.core.getDeviceContext() };
911
+ if (data.deeplinkId) ctx.deeplinkId = data.deeplinkId;
912
+ else delete ctx.deeplinkId;
913
+ this.core.setDeviceContext(ctx);
914
+ } catch {}
915
+ try {
916
+ if (typeof localStorage !== "undefined") localStorage.setItem(ATTRIBUTION_DATA_KEY, JSON.stringify(data));
917
+ } catch {}
918
+ if (this.enableDebug) console.log(`[Layers] setAttributionData(${JSON.stringify(data)})`);
919
+ }
920
+ /**
921
+ * Set a listener to receive SDK initialization timing metrics.
922
+ * Must be called **before** `init()` to receive the callback.
923
+ * Pass `null` to clear the listener.
924
+ */
925
+ setInitListener(listener) {
926
+ this._initListener = listener;
927
+ }
928
+ /**
929
+ * Enable the debug overlay — a lightweight DOM panel showing SDK state.
930
+ * The overlay updates every second with queue depth, session info, and recent events.
931
+ */
932
+ enableDebugOverlay() {
933
+ this._debugOverlayActive = true;
934
+ enableDebugOverlay(() => ({
935
+ sdkVersion: SDK_VERSION,
936
+ queueDepth: this.core.queueDepth(),
937
+ sessionId: this.core.getSessionId(),
938
+ installId: this._installId,
939
+ appId: this.config.appId,
940
+ environment: this.config.environment ?? "production",
941
+ isOnline: this.isOnline,
942
+ recentEvents: this._recentEvents
943
+ }));
944
+ }
945
+ /**
946
+ * Disable and remove the debug overlay from the DOM.
947
+ */
948
+ disableDebugOverlay() {
949
+ this._debugOverlayActive = false;
950
+ disableDebugOverlay();
951
+ }
952
+ /** Whether the SDK has completed async initialization. */
953
+ get isInitialized() {
954
+ return this._isInitialized;
955
+ }
956
+ /** SDK version string. */
957
+ getSdkVersion() {
958
+ return SDK_VERSION;
959
+ }
960
+ /**
961
+ * Returns the most recent tracked events (newest first).
962
+ * Each entry is a formatted string like "12:34:56 event_name (3 props)".
963
+ */
964
+ getRecentEvents() {
965
+ return this._recentEvents;
966
+ }
967
+ /** Flush all queued events to the server. Falls back to synchronous persistence on failure. */
968
+ async flush() {
969
+ try {
970
+ await this.core.flushAsync();
971
+ } catch (e) {
972
+ this.emitError(e);
973
+ this.core.flush();
974
+ }
975
+ }
976
+ /** Immediately shut down the client, removing all event listeners. Queued events are persisted but not flushed. */
977
+ shutdown() {
978
+ this.cleanupListeners();
979
+ if (this._debugOverlayActive) this.disableDebugOverlay();
980
+ this._attributionData = null;
981
+ this._initListener = null;
982
+ this.core.shutdown();
983
+ }
984
+ /**
985
+ * Async shutdown: flushes remaining events before shutting down.
986
+ * @param timeoutMs Maximum time to wait for flush (default 3000ms).
987
+ */
988
+ async shutdownAsync(timeoutMs = 3e3) {
989
+ this.cleanupListeners();
990
+ if (this._debugOverlayActive) this.disableDebugOverlay();
991
+ this._attributionData = null;
992
+ this._initListener = null;
993
+ try {
994
+ await Promise.race([this.core.flushAsync(), new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
995
+ } catch {}
996
+ this.core.shutdown();
997
+ }
998
+ cleanupListeners() {
999
+ if (typeof window !== "undefined") {
1000
+ if (this.onlineListener) {
1001
+ window.removeEventListener("online", this.onlineListener);
1002
+ this.onlineListener = null;
1003
+ }
1004
+ if (this.offlineListener) {
1005
+ window.removeEventListener("offline", this.offlineListener);
1006
+ this.offlineListener = null;
1007
+ }
1008
+ if (this.beforeUnloadListener) {
1009
+ window.removeEventListener("beforeunload", this.beforeUnloadListener);
1010
+ this.beforeUnloadListener = null;
1011
+ }
1012
+ }
1013
+ if (typeof document !== "undefined" && this.visibilityListener) {
1014
+ document.removeEventListener("visibilitychange", this.visibilityListener);
1015
+ this.visibilityListener = null;
1016
+ }
1017
+ if (this.deepLinkUnsubscribe) {
1018
+ this.deepLinkUnsubscribe();
1019
+ this.deepLinkUnsubscribe = null;
1020
+ }
1021
+ }
1022
+ /** End the current session and start a new one with a fresh session ID. */
1023
+ startNewSession() {
1024
+ this.core.startNewSession();
1025
+ }
1026
+ /**
1027
+ * Register an error listener. Errors from track/screen/flush
1028
+ * that would otherwise be silently dropped are forwarded here.
1029
+ */
1030
+ on(event, listener) {
1031
+ if (event === "error") this.errorListeners.add(listener);
1032
+ return this;
1033
+ }
1034
+ /**
1035
+ * Remove a previously registered error listener.
1036
+ */
1037
+ off(event, listener) {
1038
+ if (event === "error") this.errorListeners.delete(listener);
1039
+ return this;
1040
+ }
1041
+ emitError(error) {
1042
+ const err = error instanceof Error ? error : new Error(String(error));
1043
+ for (const listener of this.errorListeners) try {
1044
+ listener(err);
1045
+ } catch {}
1046
+ if (this.enableDebug && this.errorListeners.size === 0) console.warn("[Layers]", err.message);
1047
+ }
1048
+ initializeInstallId() {
1049
+ if (typeof localStorage === "undefined") return;
1050
+ try {
1051
+ const existing = localStorage.getItem(INSTALL_ID_KEY);
1052
+ if (existing) {
1053
+ this._installId = existing;
1054
+ this._hadPriorSdkState = true;
1055
+ } else {
1056
+ const newId = generateUUID();
1057
+ localStorage.setItem(INSTALL_ID_KEY, newId);
1058
+ this._installId = newId;
1059
+ this._hadPriorSdkState = false;
1060
+ if (!localStorage.getItem(FIRST_INSTALL_TIME_KEY)) localStorage.setItem(FIRST_INSTALL_TIME_KEY, String(Date.now()));
1061
+ }
1062
+ } catch {
1063
+ this._installId = generateUUID();
1064
+ }
1065
+ }
1066
+ shouldTreatAsNewInstall() {
1067
+ if (this.storageGet(FIRST_LAUNCH_TRACKED_KEY) === "true") return false;
1068
+ if (this._hadPriorSdkState) return true;
1069
+ const installTimeStr = this.storageGet(FIRST_INSTALL_TIME_KEY);
1070
+ if (installTimeStr) {
1071
+ const installTime = Number(installTimeStr);
1072
+ if (Number.isFinite(installTime)) {
1073
+ const elapsed = Date.now() - installTime;
1074
+ const isRecentInstall = elapsed <= INSTALL_EVENT_MAX_DIFF_MS;
1075
+ if (!isRecentInstall && this.enableDebug) console.log(`[Layers] Install event gated: installed ${Math.round(elapsed / 1e3)}s ago (threshold=${INSTALL_EVENT_MAX_DIFF_MS / 1e3}s), no prior SDK state — suppressing is_first_launch`);
1076
+ return isRecentInstall;
1077
+ }
1078
+ }
1079
+ return true;
1080
+ }
1081
+ restoreAttributionData() {
1082
+ try {
1083
+ if (typeof localStorage === "undefined") return;
1084
+ const raw = localStorage.getItem(ATTRIBUTION_DATA_KEY);
1085
+ if (!raw) return;
1086
+ const data = JSON.parse(raw);
1087
+ this._attributionData = data;
1088
+ if (data.deeplinkId) try {
1089
+ const currentContext = this.core.getDeviceContext();
1090
+ this.core.setDeviceContext({
1091
+ ...currentContext,
1092
+ deeplinkId: data.deeplinkId
1093
+ });
1094
+ } catch {}
1095
+ if (this.enableDebug && data) console.log(`[Layers] Restored attribution data: ${JSON.stringify(data)}`);
1096
+ } catch {}
1097
+ }
1098
+ /**
1099
+ * Merge attribution properties into event properties.
1100
+ * deeplink_id flows through DeviceContext; other fields flow through properties.
1101
+ */
1102
+ mergeAttributionProperties(properties) {
1103
+ const data = this._attributionData;
1104
+ if (!data) return properties;
1105
+ const merged = { ...properties ?? {} };
1106
+ if (data.gclid) merged.gclid = data.gclid;
1107
+ if (data.fbclid) merged.fbclid = data.fbclid;
1108
+ if (data.ttclid) merged.ttclid = data.ttclid;
1109
+ if (data.msclkid) merged.msclkid = data.msclkid;
1110
+ return merged;
1111
+ }
1112
+ /** Merge CAPI, attribution (url-based), setAttributionData, and user properties. */
1113
+ mergeAllProperties(properties) {
1114
+ const capiProps = getCapiProperties();
1115
+ const urlAttrProps = getAttributionProperties();
1116
+ const attrProps = this.mergeAttributionProperties(void 0) ?? {};
1117
+ return {
1118
+ ...capiProps,
1119
+ ...urlAttrProps,
1120
+ ...attrProps,
1121
+ ...properties
1122
+ };
1123
+ }
1124
+ checkClipboardAttribution() {
1125
+ if (typeof navigator === "undefined" || typeof localStorage === "undefined") return;
1126
+ try {
1127
+ if (localStorage.getItem(CLIPBOARD_CHECKED_KEY) === "true") return;
1128
+ } catch {
1129
+ return;
1130
+ }
1131
+ let clipboardEnabled = false;
1132
+ try {
1133
+ const configJson = this.core.getRemoteConfigJson?.();
1134
+ if (configJson) clipboardEnabled = JSON.parse(configJson)?.clipboard_attribution_enabled === true;
1135
+ } catch {}
1136
+ this.storageSet(CLIPBOARD_CHECKED_KEY, "true");
1137
+ if (!clipboardEnabled) return;
1138
+ if (navigator.clipboard?.readText) navigator.clipboard.readText().then((text) => {
1139
+ if (!text) return;
1140
+ const match = text.match(/https?:\/\/(in\.layers\.com|link\.layers\.com)\/c\/([^?\s]+)/);
1141
+ if (match) {
1142
+ this.track("clipboard_attribution", {
1143
+ clipboard_url: text,
1144
+ clipboard_click_id: match[2]
1145
+ });
1146
+ if (this.enableDebug) console.log(`[Layers] Clipboard attribution found: ${match[2]}`);
1147
+ }
1148
+ }).catch(() => {});
1149
+ }
1150
+ _trackedDeepLinkUrls = /* @__PURE__ */ new Set();
1151
+ setupDeepLinkAutoTracking() {
1152
+ this.deepLinkUnsubscribe = setupDeepLinkListener((data) => {
1153
+ if (this._trackedDeepLinkUrls.has(data.url)) return;
1154
+ this._trackedDeepLinkUrls.add(data.url);
1155
+ setTimeout(() => this._trackedDeepLinkUrls.delete(data.url), 2e3);
1156
+ try {
1157
+ const properties = {
1158
+ ...data.queryParams,
1159
+ url: data.url,
1160
+ scheme: data.scheme,
1161
+ host: data.host,
1162
+ path: data.path
1163
+ };
1164
+ this.track("deep_link_opened", properties);
1165
+ if (this.enableDebug) console.log(`[Layers] auto-tracked deep_link_opened: ${data.url}`);
1166
+ } catch (e) {
1167
+ this.emitError(e);
1168
+ }
1169
+ });
1170
+ }
1171
+ trackInitTiming(mainThreadDurationMs, totalDurationMs) {
1172
+ try {
1173
+ this.core.track("layers_init_timing", {
1174
+ duration_ms: totalDurationMs,
1175
+ main_thread_duration_ms: mainThreadDurationMs
1176
+ }, this.appUserId);
1177
+ } catch {}
1178
+ }
1179
+ recordRecentEvent(eventName, properties) {
1180
+ const now = /* @__PURE__ */ new Date();
1181
+ const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
1182
+ const propCount = Object.keys(properties ?? {}).length;
1183
+ const entry = `${time} ${eventName}${propCount > 0 ? ` (${String(propCount)} props)` : ""}`;
1184
+ this._recentEvents.unshift(entry);
1185
+ if (this._recentEvents.length > LayersClient.MAX_RECENT_EVENTS) this._recentEvents.pop();
1186
+ }
1187
+ initializeDeviceInfo() {
1188
+ const context = {
1189
+ platform: "web",
1190
+ osVersion: this.detectOS(),
1191
+ appVersion: SDK_VERSION,
1192
+ deviceModel: this.detectDeviceModel(),
1193
+ locale: this.detectLocale(),
1194
+ screenSize: this.detectScreenSize(),
1195
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1196
+ ...this._installId != null && { installId: this._installId }
1197
+ };
1198
+ this.core.setDeviceContext(context);
1199
+ }
1200
+ detectOS() {
1201
+ if (typeof navigator === "undefined") return "unknown";
1202
+ const ua = navigator.userAgent;
1203
+ if (ua.includes("Windows")) return "Windows";
1204
+ if (ua.includes("Mac OS")) return "macOS";
1205
+ if (ua.includes("Linux")) return "Linux";
1206
+ if (ua.includes("Android")) return "Android";
1207
+ if (ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
1208
+ return "unknown";
1209
+ }
1210
+ detectDeviceModel() {
1211
+ if (typeof navigator === "undefined") return "unknown";
1212
+ if ("userAgentData" in navigator) {
1213
+ const uaData = navigator.userAgentData;
1214
+ if (uaData?.platform) return uaData.platform;
1215
+ }
1216
+ return navigator.platform ?? "unknown";
1217
+ }
1218
+ detectLocale() {
1219
+ if (typeof navigator === "undefined") return "en-US";
1220
+ return navigator.language ?? "en-US";
1221
+ }
1222
+ detectScreenSize() {
1223
+ if (typeof window === "undefined" || typeof screen === "undefined") return "unknown";
1224
+ return `${screen.width}x${screen.height}`;
1225
+ }
1226
+ setupNetworkListener() {
1227
+ if (typeof window === "undefined") return;
1228
+ this.isOnline = navigator?.onLine ?? true;
1229
+ this.onlineListener = () => {
1230
+ this.isOnline = true;
1231
+ this.core.flushAsync().catch(() => {});
1232
+ };
1233
+ this.offlineListener = () => {
1234
+ this.isOnline = false;
1235
+ };
1236
+ window.addEventListener("online", this.onlineListener);
1237
+ window.addEventListener("offline", this.offlineListener);
1238
+ }
1239
+ getBeaconUrl() {
1240
+ return `${this.baseUrl}/events`;
1241
+ }
1242
+ setupLifecycleListeners() {
1243
+ if (typeof window === "undefined" || typeof document === "undefined") return;
1244
+ this.visibilityListener = () => {
1245
+ if (document.visibilityState === "hidden") {
1246
+ try {
1247
+ this.core.track("app_background", {}, this.appUserId);
1248
+ this.recordRecentEvent("app_background");
1249
+ } catch {}
1250
+ if (typeof navigator !== "undefined" && navigator.sendBeacon && this.core.queueDepth() > 0) try {
1251
+ const batch = this.core.createBeaconPayload();
1252
+ if (batch) {
1253
+ if (navigator.sendBeacon(this.getBeaconUrl(), batch)) {
1254
+ this.core.clearBeaconEvents();
1255
+ return;
1256
+ }
1257
+ this.core.requeueBeaconEvents();
1258
+ this.core.flush();
1259
+ return;
1260
+ }
1261
+ } catch {
1262
+ this.core.requeueBeaconEvents();
1263
+ }
1264
+ this.core.flush();
1265
+ } else if (document.visibilityState === "visible") {
1266
+ try {
1267
+ this.core.track("app_foreground", {}, this.appUserId);
1268
+ this.recordRecentEvent("app_foreground");
1269
+ } catch {}
1270
+ if (this.isOnline) this.core.flushAsync().catch(() => {});
1271
+ }
1272
+ };
1273
+ document.addEventListener("visibilitychange", this.visibilityListener);
1274
+ this.beforeUnloadListener = () => {
1275
+ this.core.flush();
1276
+ };
1277
+ window.addEventListener("beforeunload", this.beforeUnloadListener);
1278
+ }
1279
+ storageGet(key) {
1280
+ if (typeof localStorage === "undefined") return null;
1281
+ try {
1282
+ return localStorage.getItem(key);
1283
+ } catch {
1284
+ return null;
1285
+ }
1286
+ }
1287
+ storageSet(key, value) {
1288
+ if (typeof localStorage === "undefined") return;
1289
+ try {
1290
+ localStorage.setItem(key, value);
1291
+ } catch {}
1292
+ }
1293
+ };
1294
+ function generateUUID() {
1295
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
1296
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1297
+ const r = Math.floor(Math.random() * 16);
1298
+ return (c === "x" ? r : r % 4 + 8).toString(16);
1299
+ });
1300
+ }
1301
+ const SDK_VERSION = typeof __LAYERS_CLIENT_VERSION__ !== "undefined" ? __LAYERS_CLIENT_VERSION__ : "0.1.1-alpha.1";
1302
+
1303
+ //#endregion
1304
+ export { parseDeepLink as A, shareEvent as C, tutorialCompleteEvent as D, subscribeEvent as E, getFbpCookie as F, getPageUrl as I, getTtpCookie as L, formatFbc as M, getCapiProperties as N, viewContentEvent as O, getFbc as P, getAttribution as R, searchEvent as S, startTrialEvent as T, levelStartEvent as _, trackOrder as a, registerEvent as b, trackRefund as c, trackViewProduct as d, StandardEvents as f, levelCompleteEvent as g, initiateCheckoutEvent as h, trackBeginCheckout as i, setupDeepLinkListener as j, viewItemEvent as k, trackRemoveFromCart as l, addToWishlistEvent as m, LayersError$1 as n, trackPurchase as o, addToCartEvent as p, trackAddToCart as r, trackPurchaseFailed as s, LayersClient as t, trackSubscription as u, loginEvent as v, signUpEvent as w, screenViewEvent as x, purchaseEvent as y, getAttributionProperties as z };
1305
+ //# sourceMappingURL=src-DnOWq7k2.js.map