@cloudsignal/pwa-sdk 1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2026 @@
1
+ /**
2
+ * CloudSignal PWA SDK v1.0.0
3
+ * https://cloudsignal.io
4
+ * MIT License
5
+ */
6
+
7
+ // src/utils/fingerprint.ts
8
+ async function generateBrowserFingerprint() {
9
+ try {
10
+ const components = [];
11
+ components.push(`${screen.width}x${screen.height}`);
12
+ components.push(`${screen.colorDepth}`);
13
+ components.push(Intl.DateTimeFormat().resolvedOptions().timeZone);
14
+ components.push(navigator.language);
15
+ components.push(navigator.platform);
16
+ if (navigator.hardwareConcurrency) {
17
+ components.push(navigator.hardwareConcurrency.toString());
18
+ }
19
+ if (navigator.deviceMemory) {
20
+ components.push(navigator.deviceMemory.toString());
21
+ }
22
+ const canvasFingerprint = getCanvasFingerprint();
23
+ if (canvasFingerprint) {
24
+ components.push(canvasFingerprint);
25
+ }
26
+ const webglFingerprint = getWebGLFingerprint();
27
+ if (webglFingerprint) {
28
+ components.push(webglFingerprint);
29
+ }
30
+ const audioFingerprint = await getAudioFingerprint();
31
+ if (audioFingerprint) {
32
+ components.push(audioFingerprint);
33
+ }
34
+ const combined = components.join("|");
35
+ const hash = await hashString(combined);
36
+ return hash;
37
+ } catch (error) {
38
+ console.warn("Browser fingerprint generation failed:", error);
39
+ return null;
40
+ }
41
+ }
42
+ function getCanvasFingerprint() {
43
+ try {
44
+ const canvas = document.createElement("canvas");
45
+ const ctx = canvas.getContext("2d");
46
+ if (!ctx) return null;
47
+ canvas.width = 200;
48
+ canvas.height = 50;
49
+ ctx.textBaseline = "alphabetic";
50
+ ctx.fillStyle = "#f60";
51
+ ctx.fillRect(125, 1, 62, 20);
52
+ ctx.fillStyle = "#069";
53
+ ctx.font = "11pt Arial";
54
+ ctx.fillText("CloudSignal PWA", 2, 15);
55
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
56
+ ctx.font = "18pt Arial";
57
+ ctx.fillText("CloudSignal PWA", 4, 45);
58
+ const dataUrl = canvas.toDataURL();
59
+ return dataUrl.slice(-50);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ function getWebGLFingerprint() {
65
+ try {
66
+ const canvas = document.createElement("canvas");
67
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
68
+ if (!gl) return null;
69
+ const webglCtx = gl;
70
+ const debugInfo = webglCtx.getExtension("WEBGL_debug_renderer_info");
71
+ if (debugInfo) {
72
+ const vendor2 = webglCtx.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
73
+ const renderer2 = webglCtx.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
74
+ return `${vendor2}~${renderer2}`;
75
+ }
76
+ const vendor = webglCtx.getParameter(webglCtx.VENDOR);
77
+ const renderer = webglCtx.getParameter(webglCtx.RENDERER);
78
+ return `${vendor}~${renderer}`;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ async function getAudioFingerprint() {
84
+ try {
85
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
86
+ if (!AudioContextClass) return null;
87
+ const audioContext = new AudioContextClass();
88
+ const oscillator = audioContext.createOscillator();
89
+ const analyser = audioContext.createAnalyser();
90
+ const gainNode = audioContext.createGain();
91
+ const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
92
+ gainNode.gain.value = 0;
93
+ oscillator.type = "triangle";
94
+ oscillator.connect(analyser);
95
+ analyser.connect(scriptProcessor);
96
+ scriptProcessor.connect(gainNode);
97
+ gainNode.connect(audioContext.destination);
98
+ oscillator.start(0);
99
+ await new Promise((resolve) => setTimeout(resolve, 100));
100
+ const frequencyData = new Uint8Array(analyser.frequencyBinCount);
101
+ analyser.getByteFrequencyData(frequencyData);
102
+ let sum = 0;
103
+ for (let i = 0; i < frequencyData.length; i++) {
104
+ sum += frequencyData[i];
105
+ }
106
+ oscillator.stop();
107
+ await audioContext.close();
108
+ return sum.toString(36);
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+ async function hashString(str) {
114
+ const encoder = new TextEncoder();
115
+ const data = encoder.encode(str);
116
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
117
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
118
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
119
+ }
120
+ function generateTrackingId(os, osVersion, browser, browserVersion, deviceModel) {
121
+ const parts = [
122
+ os,
123
+ osVersion,
124
+ browser,
125
+ browserVersion,
126
+ deviceModel
127
+ ].filter(Boolean);
128
+ return parts.join("_").replace(/[^a-zA-Z0-9_]/g, "_");
129
+ }
130
+
131
+ // src/DeviceDetector.ts
132
+ var DeviceDetector = class {
133
+ constructor() {
134
+ this.cachedInfo = null;
135
+ }
136
+ /**
137
+ * Get comprehensive device information
138
+ */
139
+ getDeviceInfo() {
140
+ if (this.cachedInfo) {
141
+ return this.cachedInfo;
142
+ }
143
+ const platform = this.getPlatformInfo();
144
+ const screen2 = this.getScreenInfo();
145
+ const network = this.getNetworkInfo();
146
+ const capabilities = this.getCapabilities();
147
+ const info = {
148
+ // Operating System
149
+ os: platform.os,
150
+ osVersion: platform.osVersion,
151
+ // Device
152
+ deviceType: platform.deviceType,
153
+ deviceModel: platform.deviceModel,
154
+ isMobile: this.isMobile(),
155
+ isTablet: this.isTablet(),
156
+ isDesktop: this.isDesktop(),
157
+ isWebView: platform.isWebView,
158
+ // Browser
159
+ browser: platform.browser,
160
+ browserVersion: platform.browserVersion,
161
+ // Platform Flags
162
+ isIOS: platform.os === "iOS",
163
+ isAndroid: platform.os === "Android",
164
+ isMacOS: platform.os === "macOS",
165
+ isWindows: platform.os === "Windows",
166
+ isLinux: platform.os === "Linux",
167
+ // Screen
168
+ screenWidth: screen2.width,
169
+ screenHeight: screen2.height,
170
+ pixelRatio: screen2.pixelRatio,
171
+ // PWA Capabilities
172
+ supportLevel: capabilities.supportLevel,
173
+ hasNotificationPermission: capabilities.notifications,
174
+ hasPushManager: capabilities.push,
175
+ hasServiceWorker: capabilities.serviceWorker,
176
+ hasShareAPI: capabilities.share,
177
+ hasBadgeAPI: capabilities.badge,
178
+ // Permissions
179
+ notificationPermission: this.getNotificationPermission(),
180
+ // Network
181
+ isOnline: network.isOnline,
182
+ connectionType: network.connectionType,
183
+ // Analytics
184
+ platformIcon: this.getPlatformIcon(platform.os, platform.deviceType),
185
+ userAgent: platform.userAgent,
186
+ trackingId: generateTrackingId(
187
+ platform.os,
188
+ platform.osVersion,
189
+ platform.browser,
190
+ platform.browserVersion,
191
+ platform.deviceModel
192
+ )
193
+ };
194
+ this.cachedInfo = info;
195
+ return info;
196
+ }
197
+ /**
198
+ * Clear cached device info (useful when network/permissions change)
199
+ */
200
+ clearCache() {
201
+ this.cachedInfo = null;
202
+ }
203
+ /**
204
+ * Get platform-specific information
205
+ */
206
+ getPlatformInfo() {
207
+ const userAgent = navigator.userAgent;
208
+ const platform = navigator.platform;
209
+ let os = "Unknown";
210
+ let osVersion = "Unknown";
211
+ let browser = "Unknown";
212
+ let browserVersion = "Unknown";
213
+ let deviceType = "Unknown";
214
+ let deviceModel = "Unknown";
215
+ let isWebView = false;
216
+ if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
217
+ os = "iOS";
218
+ const iosMatch = userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
219
+ if (iosMatch) {
220
+ osVersion = `${iosMatch[1]}.${iosMatch[2]}${iosMatch[3] ? "." + iosMatch[3] : ""}`;
221
+ }
222
+ if (/iPad/.test(userAgent)) {
223
+ deviceType = "iPad";
224
+ deviceModel = this.detectiPadModel(userAgent);
225
+ } else if (/iPhone/.test(userAgent)) {
226
+ deviceType = "iPhone";
227
+ deviceModel = this.detectiPhoneModel(userAgent);
228
+ } else if (/iPod/.test(userAgent)) {
229
+ deviceType = "iPod";
230
+ deviceModel = "iPod Touch";
231
+ }
232
+ } else if (/Android/.test(userAgent)) {
233
+ os = "Android";
234
+ const androidMatch = userAgent.match(/Android (\d+\.?\d*\.?\d*)/);
235
+ if (androidMatch) {
236
+ osVersion = androidMatch[1];
237
+ }
238
+ const deviceInfo = this.detectAndroidDevice(userAgent);
239
+ deviceType = deviceInfo.type;
240
+ deviceModel = deviceInfo.model;
241
+ } else if (/Mac/.test(platform)) {
242
+ os = "macOS";
243
+ const macMatch = userAgent.match(/Mac OS X (\d+)[_.](\d+)[_.]?(\d+)?/);
244
+ if (macMatch) {
245
+ osVersion = `${macMatch[1]}.${macMatch[2]}${macMatch[3] ? "." + macMatch[3] : ""}`;
246
+ }
247
+ deviceType = "Desktop";
248
+ deviceModel = "Mac";
249
+ } else if (/Win/.test(platform)) {
250
+ os = "Windows";
251
+ const winMatch = userAgent.match(/Windows NT (\d+\.\d+)/);
252
+ if (winMatch) {
253
+ osVersion = this.getWindowsVersion(winMatch[1]);
254
+ }
255
+ deviceType = "Desktop";
256
+ deviceModel = "PC";
257
+ } else if (/Linux/.test(platform)) {
258
+ os = "Linux";
259
+ deviceType = "Desktop";
260
+ deviceModel = "Linux PC";
261
+ if (/Ubuntu/.test(userAgent)) osVersion = "Ubuntu";
262
+ else if (/Fedora/.test(userAgent)) osVersion = "Fedora";
263
+ else if (/Debian/.test(userAgent)) osVersion = "Debian";
264
+ }
265
+ if (/Chrome/.test(userAgent) && !/Edg/.test(userAgent) && !/OPR/.test(userAgent)) {
266
+ browser = "Chrome";
267
+ const chromeMatch = userAgent.match(/Chrome\/(\d+\.?\d*\.?\d*\.?\d*)/);
268
+ if (chromeMatch) browserVersion = chromeMatch[1];
269
+ } else if (/Safari/.test(userAgent) && !/Chrome/.test(userAgent)) {
270
+ browser = "Safari";
271
+ const safariMatch = userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/);
272
+ if (safariMatch) browserVersion = safariMatch[1];
273
+ } else if (/Firefox/.test(userAgent)) {
274
+ browser = "Firefox";
275
+ const firefoxMatch = userAgent.match(/Firefox\/(\d+\.?\d*\.?\d*)/);
276
+ if (firefoxMatch) browserVersion = firefoxMatch[1];
277
+ } else if (/Edg/.test(userAgent)) {
278
+ browser = "Edge";
279
+ const edgeMatch = userAgent.match(/Edg\/(\d+\.?\d*\.?\d*\.?\d*)/);
280
+ if (edgeMatch) browserVersion = edgeMatch[1];
281
+ } else if (/OPR/.test(userAgent) || /Opera/.test(userAgent)) {
282
+ browser = "Opera";
283
+ const operaMatch = userAgent.match(/(?:OPR|Opera)\/(\d+\.?\d*\.?\d*)/);
284
+ if (operaMatch) browserVersion = operaMatch[1];
285
+ } else if (/SamsungBrowser/.test(userAgent)) {
286
+ browser = "Samsung Internet";
287
+ const samsungMatch = userAgent.match(/SamsungBrowser\/(\d+\.?\d*)/);
288
+ if (samsungMatch) browserVersion = samsungMatch[1];
289
+ }
290
+ isWebView = this.detectWebView(userAgent);
291
+ return {
292
+ os,
293
+ osVersion,
294
+ browser,
295
+ browserVersion,
296
+ deviceType,
297
+ deviceModel,
298
+ isWebView,
299
+ userAgent
300
+ };
301
+ }
302
+ /**
303
+ * Get screen information
304
+ */
305
+ getScreenInfo() {
306
+ return {
307
+ width: screen.width,
308
+ height: screen.height,
309
+ pixelRatio: window.devicePixelRatio || 1,
310
+ orientation: screen.width > screen.height ? "landscape" : "portrait"
311
+ };
312
+ }
313
+ /**
314
+ * Get network information
315
+ */
316
+ getNetworkInfo() {
317
+ const connection = navigator.connection;
318
+ return {
319
+ isOnline: navigator.onLine,
320
+ connectionType: connection?.effectiveType || "unknown",
321
+ effectiveType: connection?.effectiveType,
322
+ downlink: connection?.downlink,
323
+ rtt: connection?.rtt,
324
+ saveData: connection?.saveData
325
+ };
326
+ }
327
+ /**
328
+ * Get PWA capabilities
329
+ */
330
+ getCapabilities() {
331
+ const capabilities = {
332
+ serviceWorker: "serviceWorker" in navigator,
333
+ push: "PushManager" in window,
334
+ notifications: "Notification" in window,
335
+ backgroundSync: "serviceWorker" in navigator && "SyncManager" in window,
336
+ badge: "setAppBadge" in navigator,
337
+ share: "share" in navigator,
338
+ shareTarget: "launchQueue" in window,
339
+ fileSystemAccess: "showOpenFilePicker" in window,
340
+ contactPicker: "ContactsManager" in window,
341
+ periodicSync: "serviceWorker" in navigator && "PeriodicSyncManager" in window,
342
+ supportLevel: "none"
343
+ };
344
+ if (capabilities.serviceWorker && capabilities.push && capabilities.notifications) {
345
+ capabilities.supportLevel = "full";
346
+ } else if (capabilities.serviceWorker && capabilities.notifications) {
347
+ capabilities.supportLevel = "partial";
348
+ } else if (capabilities.serviceWorker) {
349
+ capabilities.supportLevel = "basic";
350
+ }
351
+ return capabilities;
352
+ }
353
+ /**
354
+ * Get notification permission state
355
+ */
356
+ getNotificationPermission() {
357
+ if (!("Notification" in window)) {
358
+ return "denied";
359
+ }
360
+ return Notification.permission;
361
+ }
362
+ /**
363
+ * Check if device is mobile
364
+ */
365
+ isMobile() {
366
+ const userAgent = navigator.userAgent;
367
+ return /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
368
+ }
369
+ /**
370
+ * Check if device is tablet
371
+ */
372
+ isTablet() {
373
+ const userAgent = navigator.userAgent;
374
+ return /iPad|Android(?!.*Mobile)|Tablet/i.test(userAgent);
375
+ }
376
+ /**
377
+ * Check if device is desktop
378
+ */
379
+ isDesktop() {
380
+ return !this.isMobile() && !this.isTablet();
381
+ }
382
+ /**
383
+ * Get platform icon emoji
384
+ */
385
+ getPlatformIcon(os, deviceType) {
386
+ if (os === "iOS" || deviceType === "iPhone" || deviceType === "iPad") {
387
+ return "\u{1F4F1}";
388
+ }
389
+ if (os === "Android") {
390
+ return "\u{1F916}";
391
+ }
392
+ if (os === "macOS") {
393
+ return "\u{1F4BB}";
394
+ }
395
+ return "\u{1F5A5}\uFE0F";
396
+ }
397
+ /**
398
+ * Detect iPhone model from user agent
399
+ */
400
+ detectiPhoneModel(userAgent) {
401
+ const screenHeight = screen.height;
402
+ const screenWidth = screen.width;
403
+ if (screenHeight === 932 || screenWidth === 932) return "iPhone 15 Pro Max/14 Pro Max";
404
+ if (screenHeight === 896 || screenWidth === 896) return "iPhone 15 Pro/14 Pro/11 Pro Max";
405
+ if (screenHeight === 852 || screenWidth === 852) return "iPhone 15/14";
406
+ if (screenHeight === 844 || screenWidth === 844) return "iPhone 13/12";
407
+ if (screenHeight === 812 || screenWidth === 812) return "iPhone X/XS/11 Pro";
408
+ if (screenHeight === 736 || screenWidth === 736) return "iPhone 8 Plus";
409
+ if (screenHeight === 667 || screenWidth === 667) return "iPhone 8/SE";
410
+ if (screenHeight === 568 || screenWidth === 568) return "iPhone SE (1st)";
411
+ return "iPhone";
412
+ }
413
+ /**
414
+ * Detect iPad model from user agent
415
+ */
416
+ detectiPadModel(userAgent) {
417
+ const screenWidth = screen.width;
418
+ const screenHeight = screen.height;
419
+ const pixelRatio = window.devicePixelRatio || 1;
420
+ if ((screenHeight === 1366 || screenWidth === 1366) && pixelRatio === 2) {
421
+ return 'iPad Pro 12.9"';
422
+ }
423
+ if ((screenHeight === 1194 || screenWidth === 1194) && pixelRatio === 2) {
424
+ return 'iPad Pro 11"';
425
+ }
426
+ if ((screenHeight === 1180 || screenWidth === 1180) && pixelRatio === 2) {
427
+ return "iPad Air/10th Gen";
428
+ }
429
+ if ((screenHeight === 1133 || screenWidth === 1133) && pixelRatio === 2) {
430
+ return "iPad mini";
431
+ }
432
+ if (screenHeight === 1024 || screenWidth === 1024) {
433
+ return "iPad";
434
+ }
435
+ return "iPad";
436
+ }
437
+ /**
438
+ * Detect Android device model
439
+ */
440
+ detectAndroidDevice(userAgent) {
441
+ let type = "Phone";
442
+ let model = "Android Device";
443
+ if (/Tablet|SM-T|Tab|GT-P|MediaPad/i.test(userAgent)) {
444
+ type = "Tablet";
445
+ }
446
+ if (/SM-S9\d{2}/i.test(userAgent)) model = "Samsung Galaxy S24";
447
+ else if (/SM-S91\d/i.test(userAgent)) model = "Samsung Galaxy S23";
448
+ else if (/SM-S90\d/i.test(userAgent)) model = "Samsung Galaxy S22";
449
+ else if (/SM-G99\d/i.test(userAgent)) model = "Samsung Galaxy S21";
450
+ else if (/SM-N9\d{2}/i.test(userAgent)) model = "Samsung Galaxy Note";
451
+ else if (/SM-A\d{2}/i.test(userAgent)) model = "Samsung Galaxy A Series";
452
+ else if (/SM-/i.test(userAgent)) {
453
+ const match = userAgent.match(/SM-[A-Z]\d+/i);
454
+ if (match) model = `Samsung ${match[0]}`;
455
+ }
456
+ if (/Pixel 8/i.test(userAgent)) model = "Google Pixel 8";
457
+ else if (/Pixel 7/i.test(userAgent)) model = "Google Pixel 7";
458
+ else if (/Pixel 6/i.test(userAgent)) model = "Google Pixel 6";
459
+ else if (/Pixel/i.test(userAgent)) model = "Google Pixel";
460
+ if (/OnePlus/i.test(userAgent)) {
461
+ const match = userAgent.match(/OnePlus[\s]?(\w+)/i);
462
+ model = match ? `OnePlus ${match[1]}` : "OnePlus";
463
+ }
464
+ if (/Xiaomi|Redmi|POCO|Mi\s/i.test(userAgent)) {
465
+ if (/Redmi/i.test(userAgent)) model = "Xiaomi Redmi";
466
+ else if (/POCO/i.test(userAgent)) model = "Xiaomi POCO";
467
+ else model = "Xiaomi";
468
+ }
469
+ if (/HUAWEI|Honor/i.test(userAgent)) {
470
+ model = /Honor/i.test(userAgent) ? "Honor" : "Huawei";
471
+ }
472
+ if (/OPPO/i.test(userAgent)) model = "OPPO";
473
+ if (/vivo/i.test(userAgent)) model = "Vivo";
474
+ return { type, model };
475
+ }
476
+ /**
477
+ * Get Windows version from NT version
478
+ */
479
+ getWindowsVersion(ntVersion) {
480
+ const versionMap = {
481
+ "10.0": "Windows 10/11",
482
+ "6.3": "Windows 8.1",
483
+ "6.2": "Windows 8",
484
+ "6.1": "Windows 7",
485
+ "6.0": "Windows Vista",
486
+ "5.1": "Windows XP"
487
+ };
488
+ return versionMap[ntVersion] || `Windows NT ${ntVersion}`;
489
+ }
490
+ /**
491
+ * Detect if running in WebView
492
+ */
493
+ detectWebView(userAgent) {
494
+ if (/FBAN|FBAV/i.test(userAgent)) return true;
495
+ if (/Instagram/i.test(userAgent)) return true;
496
+ if (/LinkedIn/i.test(userAgent)) return true;
497
+ if (/Twitter/i.test(userAgent)) return true;
498
+ if (/MicroMessenger/i.test(userAgent)) return true;
499
+ if (/Snapchat/i.test(userAgent)) return true;
500
+ if (/BytedanceWebview|TikTok/i.test(userAgent)) return true;
501
+ if (/wv|WebView/i.test(userAgent)) return true;
502
+ return false;
503
+ }
504
+ };
505
+ var deviceDetector = new DeviceDetector();
506
+
507
+ // src/ServiceWorkerManager.ts
508
+ var ServiceWorkerManager = class {
509
+ constructor(options = {}) {
510
+ this.registration = null;
511
+ this.config = {
512
+ path: "/service-worker.js",
513
+ scope: "/",
514
+ autoRegister: true,
515
+ updateBehavior: "auto",
516
+ ...options.config
517
+ };
518
+ this.debug = options.debug ?? false;
519
+ this.onRegistered = options.onRegistered;
520
+ this.onUpdated = options.onUpdated;
521
+ this.onError = options.onError;
522
+ }
523
+ /**
524
+ * Check if service workers are supported
525
+ */
526
+ isSupported() {
527
+ return "serviceWorker" in navigator;
528
+ }
529
+ /**
530
+ * Get the current service worker registration
531
+ */
532
+ getRegistration() {
533
+ return this.registration;
534
+ }
535
+ /**
536
+ * Register the service worker
537
+ */
538
+ async register() {
539
+ if (!this.isSupported()) {
540
+ this.log("Service workers not supported");
541
+ return null;
542
+ }
543
+ try {
544
+ const { path, scope } = this.getPathAndScope();
545
+ this.log(`Registering service worker: ${path} with scope ${scope}`);
546
+ const registration = await navigator.serviceWorker.register(path, { scope });
547
+ this.registration = registration;
548
+ registration.addEventListener("updatefound", () => {
549
+ this.handleUpdateFound(registration);
550
+ });
551
+ await navigator.serviceWorker.ready;
552
+ this.log("Service worker registered successfully");
553
+ this.onRegistered?.(registration);
554
+ return registration;
555
+ } catch (error) {
556
+ const err = error instanceof Error ? error : new Error(String(error));
557
+ this.log(`Service worker registration failed: ${err.message}`, "error");
558
+ this.onError?.(err);
559
+ return null;
560
+ }
561
+ }
562
+ /**
563
+ * Unregister the service worker
564
+ */
565
+ async unregister() {
566
+ if (!this.registration) {
567
+ return false;
568
+ }
569
+ try {
570
+ const result = await this.registration.unregister();
571
+ if (result) {
572
+ this.registration = null;
573
+ this.log("Service worker unregistered");
574
+ }
575
+ return result;
576
+ } catch (error) {
577
+ this.log(`Failed to unregister service worker: ${error}`, "error");
578
+ return false;
579
+ }
580
+ }
581
+ /**
582
+ * Check for service worker updates
583
+ */
584
+ async checkForUpdate() {
585
+ if (!this.registration) {
586
+ return;
587
+ }
588
+ try {
589
+ await this.registration.update();
590
+ this.log("Checked for service worker update");
591
+ } catch (error) {
592
+ this.log(`Failed to check for update: ${error}`, "error");
593
+ }
594
+ }
595
+ /**
596
+ * Wait for service worker to be ready
597
+ */
598
+ async waitForReady(timeout = 5e3) {
599
+ if (!this.isSupported()) {
600
+ return null;
601
+ }
602
+ const timeoutPromise = new Promise((resolve) => {
603
+ setTimeout(() => resolve(null), timeout);
604
+ });
605
+ try {
606
+ const registration = await Promise.race([
607
+ navigator.serviceWorker.ready,
608
+ timeoutPromise
609
+ ]);
610
+ if (registration) {
611
+ this.registration = registration;
612
+ }
613
+ return registration;
614
+ } catch {
615
+ return null;
616
+ }
617
+ }
618
+ /**
619
+ * Send a message to the service worker
620
+ */
621
+ postMessage(message) {
622
+ if (!this.registration?.active) {
623
+ this.log("No active service worker to send message to", "warn");
624
+ return;
625
+ }
626
+ this.registration.active.postMessage(message);
627
+ }
628
+ /**
629
+ * Clear app badge via service worker
630
+ */
631
+ clearBadge() {
632
+ this.postMessage({ type: "CLEAR_BADGE" });
633
+ }
634
+ /**
635
+ * Set app badge via service worker
636
+ */
637
+ setBadge(count) {
638
+ this.postMessage({ type: "SET_BADGE", count });
639
+ }
640
+ /**
641
+ * Handle service worker update found
642
+ */
643
+ handleUpdateFound(registration) {
644
+ const newWorker = registration.installing;
645
+ if (!newWorker) {
646
+ return;
647
+ }
648
+ newWorker.addEventListener("statechange", () => {
649
+ if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
650
+ this.log("New service worker available");
651
+ if (this.config.updateBehavior === "auto") {
652
+ newWorker.postMessage({ type: "SKIP_WAITING" });
653
+ }
654
+ this.onUpdated?.(registration);
655
+ }
656
+ });
657
+ }
658
+ /**
659
+ * Get service worker path and scope based on environment
660
+ * Handles Bubble.io version-live/version-test paths
661
+ */
662
+ getPathAndScope() {
663
+ if (this.config.path && this.config.scope) {
664
+ return { path: this.config.path, scope: this.config.scope };
665
+ }
666
+ try {
667
+ const pathname = window.location.pathname || "/";
668
+ if (pathname.startsWith("/version-live")) {
669
+ return {
670
+ path: "/version-live/service-worker.js",
671
+ scope: "/version-live/"
672
+ };
673
+ }
674
+ if (pathname.startsWith("/version-test")) {
675
+ return {
676
+ path: "/version-test/service-worker.js",
677
+ scope: "/version-test/"
678
+ };
679
+ }
680
+ } catch {
681
+ }
682
+ return {
683
+ path: this.config.path || "/service-worker.js",
684
+ scope: this.config.scope || "/"
685
+ };
686
+ }
687
+ /**
688
+ * Log message if debug is enabled
689
+ */
690
+ log(message, level = "log") {
691
+ if (!this.debug) return;
692
+ const prefix = "[CloudSignal PWA SW]";
693
+ console[level](`${prefix} ${message}`);
694
+ }
695
+ };
696
+
697
+ // src/InstallationManager.ts
698
+ var InstallationManager = class {
699
+ constructor(options = {}) {
700
+ this.deferredPrompt = null;
701
+ this.isInstalled = false;
702
+ this.debug = options.debug ?? false;
703
+ this.onInstallAvailable = options.onInstallAvailable;
704
+ this.onInstallAccepted = options.onInstallAccepted;
705
+ this.onInstallDismissed = options.onInstallDismissed;
706
+ this.onInstalled = options.onInstalled;
707
+ this.detectInstallationStatus();
708
+ }
709
+ /**
710
+ * Initialize event listeners
711
+ */
712
+ initialize() {
713
+ window.addEventListener("beforeinstallprompt", (event) => {
714
+ event.preventDefault();
715
+ this.deferredPrompt = event;
716
+ this.log("Install prompt available");
717
+ this.onInstallAvailable?.(this.deferredPrompt);
718
+ });
719
+ window.addEventListener("appinstalled", () => {
720
+ this.isInstalled = true;
721
+ this.deferredPrompt = null;
722
+ this.log("PWA was installed");
723
+ this.onInstalled?.();
724
+ });
725
+ if (window.matchMedia) {
726
+ const standaloneQuery = window.matchMedia("(display-mode: standalone)");
727
+ standaloneQuery.addEventListener("change", (e) => {
728
+ if (e.matches) {
729
+ this.isInstalled = true;
730
+ this.log("App is now running in standalone mode");
731
+ }
732
+ });
733
+ }
734
+ }
735
+ /**
736
+ * Get current installation state
737
+ */
738
+ getState() {
739
+ const displayMode = this.getDisplayMode();
740
+ const isIOS = this.isIOSDevice();
741
+ const isSafari = this.isSafariBrowser();
742
+ return {
743
+ isInstalled: this.isInstalled || displayMode !== "browser",
744
+ canBeInstalled: this.deferredPrompt !== null,
745
+ needsManualInstall: isIOS && isSafari && !this.isInstalled,
746
+ showManualInstructions: isIOS && isSafari && !this.isInstalled,
747
+ installSteps: this.getInstallSteps(),
748
+ displayMode
749
+ };
750
+ }
751
+ /**
752
+ * Show the install prompt
753
+ */
754
+ async showInstallPrompt() {
755
+ if (!this.deferredPrompt) {
756
+ this.log("No install prompt available", "warn");
757
+ return {
758
+ accepted: false,
759
+ outcome: "dismissed"
760
+ };
761
+ }
762
+ try {
763
+ await this.deferredPrompt.prompt();
764
+ const { outcome, platform } = await this.deferredPrompt.userChoice;
765
+ const result = {
766
+ accepted: outcome === "accepted",
767
+ outcome,
768
+ platform
769
+ };
770
+ if (outcome === "accepted") {
771
+ this.log("User accepted install prompt");
772
+ this.isInstalled = true;
773
+ this.onInstallAccepted?.(result);
774
+ } else {
775
+ this.log("User dismissed install prompt");
776
+ this.onInstallDismissed?.(result);
777
+ }
778
+ this.deferredPrompt = null;
779
+ return result;
780
+ } catch (error) {
781
+ this.log(`Error showing install prompt: ${error}`, "error");
782
+ return {
783
+ accepted: false,
784
+ outcome: "dismissed"
785
+ };
786
+ }
787
+ }
788
+ /**
789
+ * Check if install prompt is available
790
+ */
791
+ canInstall() {
792
+ return this.deferredPrompt !== null;
793
+ }
794
+ /**
795
+ * Check if PWA is installed
796
+ */
797
+ isPWAInstalled() {
798
+ return this.isInstalled || this.getDisplayMode() !== "browser";
799
+ }
800
+ /**
801
+ * Get current display mode
802
+ */
803
+ getDisplayMode() {
804
+ if (window.matchMedia("(display-mode: standalone)").matches) {
805
+ return "standalone";
806
+ }
807
+ if (window.matchMedia("(display-mode: minimal-ui)").matches) {
808
+ return "minimal-ui";
809
+ }
810
+ if (window.matchMedia("(display-mode: fullscreen)").matches) {
811
+ return "fullscreen";
812
+ }
813
+ if (navigator.standalone === true) {
814
+ return "standalone";
815
+ }
816
+ return "browser";
817
+ }
818
+ /**
819
+ * Get installation steps for current platform
820
+ */
821
+ getInstallSteps() {
822
+ if (this.isIOSDevice()) {
823
+ return [
824
+ "Tap the Share button in Safari",
825
+ 'Scroll down and tap "Add to Home Screen"',
826
+ 'Tap "Add" to install the app'
827
+ ];
828
+ }
829
+ if (this.isAndroidDevice()) {
830
+ return [
831
+ "Tap the menu (three dots) in your browser",
832
+ 'Tap "Install App" or "Add to Home Screen"',
833
+ "Follow the prompts to install"
834
+ ];
835
+ }
836
+ return [
837
+ "Click the install icon in the address bar",
838
+ "Or use the browser menu to install the app",
839
+ 'Click "Install" when prompted'
840
+ ];
841
+ }
842
+ /**
843
+ * Detect initial installation status
844
+ */
845
+ detectInstallationStatus() {
846
+ if (this.getDisplayMode() !== "browser") {
847
+ this.isInstalled = true;
848
+ return;
849
+ }
850
+ if (navigator.standalone === true) {
851
+ this.isInstalled = true;
852
+ return;
853
+ }
854
+ if ("getInstalledRelatedApps" in navigator) {
855
+ navigator.getInstalledRelatedApps().then((apps) => {
856
+ if (apps.length > 0) {
857
+ this.isInstalled = true;
858
+ }
859
+ }).catch(() => {
860
+ });
861
+ }
862
+ }
863
+ /**
864
+ * Check if device is iOS
865
+ */
866
+ isIOSDevice() {
867
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
868
+ }
869
+ /**
870
+ * Check if device is Android
871
+ */
872
+ isAndroidDevice() {
873
+ return /Android/i.test(navigator.userAgent);
874
+ }
875
+ /**
876
+ * Check if browser is Safari
877
+ */
878
+ isSafariBrowser() {
879
+ const userAgent = navigator.userAgent;
880
+ return /Safari/i.test(userAgent) && !/Chrome|CriOS|FxiOS/i.test(userAgent);
881
+ }
882
+ /**
883
+ * Log message if debug is enabled
884
+ */
885
+ log(message, level = "log") {
886
+ if (!this.debug) return;
887
+ const prefix = "[CloudSignal PWA Install]";
888
+ console[level](`${prefix} ${message}`);
889
+ }
890
+ };
891
+
892
+ // src/utils/hmac.ts
893
+ function toHex(buffer) {
894
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
895
+ }
896
+ function toBase64(buffer) {
897
+ return btoa(String.fromCharCode(...new Uint8Array(buffer)));
898
+ }
899
+ async function generateHMACSignature(secret, organizationId, timestamp, method, url, body = "") {
900
+ const encoder = new TextEncoder();
901
+ let path;
902
+ let query;
903
+ try {
904
+ const urlObj = url.startsWith("http") ? new URL(url) : new URL(url, "https://pwa.cloudsignal.app");
905
+ path = urlObj.pathname;
906
+ query = urlObj.search ? urlObj.search.slice(1) : "";
907
+ } catch {
908
+ const queryIndex = url.indexOf("?");
909
+ if (queryIndex > -1) {
910
+ path = url.slice(0, queryIndex);
911
+ query = url.slice(queryIndex + 1);
912
+ } else {
913
+ path = url;
914
+ query = "";
915
+ }
916
+ }
917
+ const canonicalParts = [
918
+ method.toUpperCase(),
919
+ path,
920
+ query,
921
+ organizationId,
922
+ timestamp
923
+ ];
924
+ if (body && body.length > 0) {
925
+ const bodyHashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(body));
926
+ const bodyHashHex = toHex(bodyHashBuffer);
927
+ canonicalParts.push(bodyHashHex);
928
+ }
929
+ const canonicalString = canonicalParts.join("\n");
930
+ const key = await crypto.subtle.importKey(
931
+ "raw",
932
+ encoder.encode(secret),
933
+ { name: "HMAC", hash: "SHA-256" },
934
+ false,
935
+ ["sign"]
936
+ );
937
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(canonicalString));
938
+ return toBase64(signature);
939
+ }
940
+ async function generateAuthHeaders(organizationId, organizationSecret, method, url, body) {
941
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
942
+ const signature = await generateHMACSignature(
943
+ organizationSecret,
944
+ organizationId,
945
+ timestamp,
946
+ method,
947
+ url,
948
+ body || ""
949
+ );
950
+ return {
951
+ "X-CloudSignal-Organization-ID": organizationId,
952
+ "X-CloudSignal-Timestamp": timestamp,
953
+ "X-CloudSignal-Signature": signature,
954
+ "Content-Type": "application/json"
955
+ };
956
+ }
957
+ async function makeAuthenticatedRequest(organizationId, organizationSecret, method, url, body) {
958
+ const bodyStr = body ? JSON.stringify(body) : void 0;
959
+ const headers = await generateAuthHeaders(
960
+ organizationId,
961
+ organizationSecret,
962
+ method,
963
+ url,
964
+ bodyStr
965
+ );
966
+ const options = {
967
+ method,
968
+ headers
969
+ };
970
+ if (bodyStr) {
971
+ options.body = bodyStr;
972
+ }
973
+ return fetch(url, options);
974
+ }
975
+ function isValidUUID(value) {
976
+ if (!value || typeof value !== "string") return false;
977
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
978
+ }
979
+
980
+ // src/utils/storage.ts
981
+ var STORAGE_PREFIX = "cloudsignal_pwa_";
982
+ function getStorageItem(key, defaultValue) {
983
+ try {
984
+ const fullKey = `${STORAGE_PREFIX}${key}`;
985
+ const item = localStorage.getItem(fullKey);
986
+ if (item === null) return defaultValue ?? null;
987
+ return JSON.parse(item);
988
+ } catch {
989
+ return defaultValue ?? null;
990
+ }
991
+ }
992
+ function setStorageItem(key, value) {
993
+ try {
994
+ const fullKey = `${STORAGE_PREFIX}${key}`;
995
+ localStorage.setItem(fullKey, JSON.stringify(value));
996
+ return true;
997
+ } catch {
998
+ return false;
999
+ }
1000
+ }
1001
+ function removeStorageItem(key) {
1002
+ try {
1003
+ const fullKey = `${STORAGE_PREFIX}${key}`;
1004
+ localStorage.removeItem(fullKey);
1005
+ return true;
1006
+ } catch {
1007
+ return false;
1008
+ }
1009
+ }
1010
+ function getRegistrationId(organizationId, serviceId) {
1011
+ const key = `registration_${organizationId}_${serviceId}`;
1012
+ return getStorageItem(key);
1013
+ }
1014
+ function setRegistrationId(organizationId, serviceId, registrationId) {
1015
+ const key = `registration_${organizationId}_${serviceId}`;
1016
+ return setStorageItem(key, registrationId);
1017
+ }
1018
+ function removeRegistrationId(organizationId, serviceId) {
1019
+ const key = `registration_${organizationId}_${serviceId}`;
1020
+ return removeStorageItem(key);
1021
+ }
1022
+ var IndexedDBStorage = class {
1023
+ constructor(dbName = "CloudSignalPWA", dbVersion = 1) {
1024
+ this.db = null;
1025
+ this.dbName = dbName;
1026
+ this.dbVersion = dbVersion;
1027
+ }
1028
+ /**
1029
+ * Initialize the database
1030
+ */
1031
+ async init() {
1032
+ return new Promise((resolve, reject) => {
1033
+ const request = indexedDB.open(this.dbName, this.dbVersion);
1034
+ request.onerror = () => reject(request.error);
1035
+ request.onsuccess = () => {
1036
+ this.db = request.result;
1037
+ resolve(this.db);
1038
+ };
1039
+ request.onupgradeneeded = (event) => {
1040
+ const db = event.target.result;
1041
+ if (!db.objectStoreNames.contains("badge")) {
1042
+ db.createObjectStore("badge");
1043
+ }
1044
+ if (!db.objectStoreNames.contains("notifications")) {
1045
+ const notificationStore = db.createObjectStore("notifications", {
1046
+ keyPath: "id",
1047
+ autoIncrement: true
1048
+ });
1049
+ notificationStore.createIndex("timestamp", "timestamp", { unique: false });
1050
+ notificationStore.createIndex("read", "read", { unique: false });
1051
+ }
1052
+ if (!db.objectStoreNames.contains("userPreferences")) {
1053
+ db.createObjectStore("userPreferences");
1054
+ }
1055
+ if (!db.objectStoreNames.contains("syncQueue")) {
1056
+ const syncStore = db.createObjectStore("syncQueue", {
1057
+ keyPath: "id",
1058
+ autoIncrement: true
1059
+ });
1060
+ syncStore.createIndex("timestamp", "timestamp", { unique: false });
1061
+ }
1062
+ };
1063
+ });
1064
+ }
1065
+ /**
1066
+ * Ensure database connection
1067
+ */
1068
+ async ensureConnection() {
1069
+ if (!this.db) {
1070
+ await this.init();
1071
+ }
1072
+ }
1073
+ /**
1074
+ * Promisify IDBRequest
1075
+ */
1076
+ promisifyRequest(request) {
1077
+ return new Promise((resolve, reject) => {
1078
+ request.onsuccess = () => resolve(request.result);
1079
+ request.onerror = () => reject(request.error);
1080
+ });
1081
+ }
1082
+ /**
1083
+ * Get badge count
1084
+ */
1085
+ async getBadgeCount() {
1086
+ await this.ensureConnection();
1087
+ const transaction = this.db.transaction(["badge"], "readonly");
1088
+ const store = transaction.objectStore("badge");
1089
+ const count = await this.promisifyRequest(store.get("count"));
1090
+ return count || 0;
1091
+ }
1092
+ /**
1093
+ * Set badge count
1094
+ */
1095
+ async setBadgeCount(count) {
1096
+ await this.ensureConnection();
1097
+ const transaction = this.db.transaction(["badge"], "readwrite");
1098
+ const store = transaction.objectStore("badge");
1099
+ await this.promisifyRequest(store.put(count, "count"));
1100
+ }
1101
+ /**
1102
+ * Increment badge count
1103
+ */
1104
+ async incrementBadgeCount(increment = 1) {
1105
+ const currentCount = await this.getBadgeCount();
1106
+ const newCount = Math.max(0, currentCount + increment);
1107
+ await this.setBadgeCount(newCount);
1108
+ return newCount;
1109
+ }
1110
+ /**
1111
+ * Save notification to history
1112
+ */
1113
+ async saveNotification(notification) {
1114
+ await this.ensureConnection();
1115
+ const transaction = this.db.transaction(["notifications"], "readwrite");
1116
+ const store = transaction.objectStore("notifications");
1117
+ const notificationData = {
1118
+ ...notification,
1119
+ timestamp: Date.now(),
1120
+ read: false
1121
+ };
1122
+ const key = await this.promisifyRequest(store.add(notificationData));
1123
+ return key;
1124
+ }
1125
+ /**
1126
+ * Get recent notifications
1127
+ */
1128
+ async getRecentNotifications(limit = 50) {
1129
+ await this.ensureConnection();
1130
+ const transaction = this.db.transaction(["notifications"], "readonly");
1131
+ const store = transaction.objectStore("notifications");
1132
+ const index = store.index("timestamp");
1133
+ const notifications = [];
1134
+ const cursor = index.openCursor(null, "prev");
1135
+ return new Promise((resolve, reject) => {
1136
+ cursor.onsuccess = (event) => {
1137
+ const result = event.target.result;
1138
+ if (result && notifications.length < limit) {
1139
+ notifications.push(result.value);
1140
+ result.continue();
1141
+ } else {
1142
+ resolve(notifications);
1143
+ }
1144
+ };
1145
+ cursor.onerror = () => reject(cursor.error);
1146
+ });
1147
+ }
1148
+ /**
1149
+ * Mark notification as read
1150
+ */
1151
+ async markNotificationAsRead(notificationId) {
1152
+ await this.ensureConnection();
1153
+ const transaction = this.db.transaction(["notifications"], "readwrite");
1154
+ const store = transaction.objectStore("notifications");
1155
+ const notification = await this.promisifyRequest(store.get(notificationId));
1156
+ if (notification) {
1157
+ notification.read = true;
1158
+ await this.promisifyRequest(store.put(notification));
1159
+ }
1160
+ }
1161
+ /**
1162
+ * Get user preference
1163
+ */
1164
+ async getUserPreference(key) {
1165
+ await this.ensureConnection();
1166
+ const transaction = this.db.transaction(["userPreferences"], "readonly");
1167
+ const store = transaction.objectStore("userPreferences");
1168
+ return this.promisifyRequest(store.get(key));
1169
+ }
1170
+ /**
1171
+ * Set user preference
1172
+ */
1173
+ async setUserPreference(key, value) {
1174
+ await this.ensureConnection();
1175
+ const transaction = this.db.transaction(["userPreferences"], "readwrite");
1176
+ const store = transaction.objectStore("userPreferences");
1177
+ await this.promisifyRequest(store.put(value, key));
1178
+ }
1179
+ /**
1180
+ * Add item to sync queue
1181
+ */
1182
+ async addToSyncQueue(action) {
1183
+ await this.ensureConnection();
1184
+ const transaction = this.db.transaction(["syncQueue"], "readwrite");
1185
+ const store = transaction.objectStore("syncQueue");
1186
+ const queueItem = {
1187
+ ...action,
1188
+ timestamp: Date.now(),
1189
+ retries: 0
1190
+ };
1191
+ const key = await this.promisifyRequest(store.add(queueItem));
1192
+ return key;
1193
+ }
1194
+ /**
1195
+ * Get sync queue items
1196
+ */
1197
+ async getSyncQueue() {
1198
+ await this.ensureConnection();
1199
+ const transaction = this.db.transaction(["syncQueue"], "readonly");
1200
+ const store = transaction.objectStore("syncQueue");
1201
+ return this.promisifyRequest(store.getAll());
1202
+ }
1203
+ /**
1204
+ * Remove item from sync queue
1205
+ */
1206
+ async removeFromSyncQueue(id) {
1207
+ await this.ensureConnection();
1208
+ const transaction = this.db.transaction(["syncQueue"], "readwrite");
1209
+ const store = transaction.objectStore("syncQueue");
1210
+ await this.promisifyRequest(store.delete(id));
1211
+ }
1212
+ };
1213
+
1214
+ // src/PushNotificationManager.ts
1215
+ var PushNotificationManager = class {
1216
+ constructor(options) {
1217
+ this.serviceWorkerRegistration = null;
1218
+ this.pushSubscription = null;
1219
+ this.registrationId = null;
1220
+ this.vapidPublicKey = null;
1221
+ this.serviceUrl = options.serviceUrl;
1222
+ this.organizationId = options.organizationId;
1223
+ this.organizationSecret = options.organizationSecret;
1224
+ this.serviceId = options.serviceId;
1225
+ this.debug = options.debug ?? false;
1226
+ this.deviceDetector = new DeviceDetector();
1227
+ this.onRegistered = options.onRegistered;
1228
+ this.onUnregistered = options.onUnregistered;
1229
+ this.onError = options.onError;
1230
+ this.onPermissionDenied = options.onPermissionDenied;
1231
+ this.registrationId = getRegistrationId(this.organizationId, this.serviceId);
1232
+ }
1233
+ /**
1234
+ * Set service worker registration
1235
+ */
1236
+ setServiceWorkerRegistration(registration) {
1237
+ this.serviceWorkerRegistration = registration;
1238
+ }
1239
+ /**
1240
+ * Set VAPID public key from config
1241
+ */
1242
+ setVapidPublicKey(key) {
1243
+ this.vapidPublicKey = key;
1244
+ }
1245
+ /**
1246
+ * Get current registration ID
1247
+ */
1248
+ getRegistrationId() {
1249
+ return this.registrationId;
1250
+ }
1251
+ /**
1252
+ * Check if registered for push notifications
1253
+ */
1254
+ isRegistered() {
1255
+ return this.registrationId !== null;
1256
+ }
1257
+ /**
1258
+ * Register for push notifications
1259
+ */
1260
+ async register(options = {}) {
1261
+ try {
1262
+ if (!this.serviceWorkerRegistration) {
1263
+ throw new Error("Service worker not registered");
1264
+ }
1265
+ if (!this.vapidPublicKey) {
1266
+ throw new Error("VAPID public key not set. Call downloadConfig() first.");
1267
+ }
1268
+ const permission = await this.requestPermission();
1269
+ if (permission !== "granted") {
1270
+ this.log("Notification permission denied");
1271
+ this.onPermissionDenied?.();
1272
+ return null;
1273
+ }
1274
+ const subscription = await this.subscribeToPush();
1275
+ if (!subscription) {
1276
+ throw new Error("Failed to subscribe to push notifications");
1277
+ }
1278
+ this.pushSubscription = subscription;
1279
+ const fingerprint = await generateBrowserFingerprint();
1280
+ const deviceInfo = this.deviceDetector.getDeviceInfo();
1281
+ const platformInfo = this.deviceDetector.getPlatformInfo();
1282
+ const subscriptionJson = subscription.toJSON();
1283
+ const registrationData = {
1284
+ serviceId: this.serviceId,
1285
+ userEmail: options.userEmail,
1286
+ userId: isValidUUID(options.userId) ? options.userId : void 0,
1287
+ endpoint: subscription.endpoint,
1288
+ keys: {
1289
+ p256dh: subscriptionJson.keys?.p256dh || "",
1290
+ auth: subscriptionJson.keys?.auth || ""
1291
+ },
1292
+ browserFingerprint: fingerprint || void 0,
1293
+ deviceType: deviceInfo.deviceType,
1294
+ deviceModel: deviceInfo.deviceModel,
1295
+ browserName: platformInfo.browser,
1296
+ browserVersion: platformInfo.browserVersion,
1297
+ osName: platformInfo.os,
1298
+ osVersion: platformInfo.osVersion,
1299
+ userAgent: platformInfo.userAgent,
1300
+ displayMode: "standalone",
1301
+ isInstalled: true,
1302
+ timezone: options.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
1303
+ language: options.language || navigator.language || "en-US"
1304
+ };
1305
+ const url = `${this.serviceUrl}/api/v1/registration/register`;
1306
+ const response = await makeAuthenticatedRequest(
1307
+ this.organizationId,
1308
+ this.organizationSecret,
1309
+ "POST",
1310
+ url,
1311
+ registrationData
1312
+ );
1313
+ if (!response.ok) {
1314
+ const errorText = await response.text();
1315
+ throw new Error(`Registration failed: ${response.status} - ${errorText}`);
1316
+ }
1317
+ const result = await response.json();
1318
+ this.registrationId = result.registration_id;
1319
+ setRegistrationId(this.organizationId, this.serviceId, this.registrationId);
1320
+ const registration = {
1321
+ registrationId: result.registration_id,
1322
+ status: result.status || "active",
1323
+ createdAt: result.created_at || (/* @__PURE__ */ new Date()).toISOString(),
1324
+ isActive: true
1325
+ };
1326
+ this.log(`Push registration successful: ${registration.registrationId}`);
1327
+ this.onRegistered?.(registration);
1328
+ return registration;
1329
+ } catch (error) {
1330
+ const err = error instanceof Error ? error : new Error(String(error));
1331
+ this.log(`Push registration failed: ${err.message}`, "error");
1332
+ this.onError?.(err);
1333
+ return null;
1334
+ }
1335
+ }
1336
+ /**
1337
+ * Unregister from push notifications
1338
+ */
1339
+ async unregister() {
1340
+ try {
1341
+ if (!this.registrationId) {
1342
+ this.log("No registration to unregister");
1343
+ return true;
1344
+ }
1345
+ if (this.pushSubscription) {
1346
+ await this.pushSubscription.unsubscribe();
1347
+ this.pushSubscription = null;
1348
+ }
1349
+ const url = `${this.serviceUrl}/api/v1/registration/unregister`;
1350
+ const response = await makeAuthenticatedRequest(
1351
+ this.organizationId,
1352
+ this.organizationSecret,
1353
+ "POST",
1354
+ url,
1355
+ { registration_id: this.registrationId }
1356
+ );
1357
+ if (!response.ok) {
1358
+ this.log(`Backend unregistration failed: ${response.status}`, "warn");
1359
+ }
1360
+ removeRegistrationId(this.organizationId, this.serviceId);
1361
+ this.registrationId = null;
1362
+ this.log("Push unregistration successful");
1363
+ this.onUnregistered?.();
1364
+ return true;
1365
+ } catch (error) {
1366
+ const err = error instanceof Error ? error : new Error(String(error));
1367
+ this.log(`Push unregistration failed: ${err.message}`, "error");
1368
+ this.onError?.(err);
1369
+ return false;
1370
+ }
1371
+ }
1372
+ /**
1373
+ * Update notification preferences
1374
+ */
1375
+ async updatePreferences(preferences) {
1376
+ try {
1377
+ if (!this.registrationId) {
1378
+ throw new Error("Not registered for push notifications");
1379
+ }
1380
+ const url = `${this.serviceUrl}/api/v1/registration/update`;
1381
+ const response = await makeAuthenticatedRequest(
1382
+ this.organizationId,
1383
+ this.organizationSecret,
1384
+ "POST",
1385
+ url,
1386
+ {
1387
+ registration_id: this.registrationId,
1388
+ notification_topics: preferences.topics,
1389
+ timezone: preferences.timezone,
1390
+ language: preferences.language,
1391
+ is_active: preferences.isActive
1392
+ }
1393
+ );
1394
+ if (!response.ok) {
1395
+ throw new Error(`Update failed: ${response.status}`);
1396
+ }
1397
+ this.log("Preferences updated successfully");
1398
+ return true;
1399
+ } catch (error) {
1400
+ const err = error instanceof Error ? error : new Error(String(error));
1401
+ this.log(`Failed to update preferences: ${err.message}`, "error");
1402
+ this.onError?.(err);
1403
+ return false;
1404
+ }
1405
+ }
1406
+ /**
1407
+ * Check registration status
1408
+ */
1409
+ async checkStatus() {
1410
+ try {
1411
+ if (!this.registrationId) {
1412
+ return null;
1413
+ }
1414
+ const url = `${this.serviceUrl}/api/v1/registration/status/${this.registrationId}`;
1415
+ const response = await makeAuthenticatedRequest(
1416
+ this.organizationId,
1417
+ this.organizationSecret,
1418
+ "GET",
1419
+ url
1420
+ );
1421
+ if (!response.ok) {
1422
+ if (response.status === 404) {
1423
+ removeRegistrationId(this.organizationId, this.serviceId);
1424
+ this.registrationId = null;
1425
+ return null;
1426
+ }
1427
+ throw new Error(`Status check failed: ${response.status}`);
1428
+ }
1429
+ const status = await response.json();
1430
+ return {
1431
+ registrationId: status.registration_id,
1432
+ status: status.status,
1433
+ isActive: status.is_active,
1434
+ isOnline: status.is_online,
1435
+ lastActive: status.last_active,
1436
+ lastSeenOnline: status.last_seen_online,
1437
+ lastHeartbeat: status.last_heartbeat,
1438
+ installationDate: status.installation_date,
1439
+ notificationCount: status.notification_count,
1440
+ deviceInfo: status.device_info
1441
+ };
1442
+ } catch (error) {
1443
+ const err = error instanceof Error ? error : new Error(String(error));
1444
+ this.log(`Status check failed: ${err.message}`, "error");
1445
+ return null;
1446
+ }
1447
+ }
1448
+ /**
1449
+ * Request notification permission
1450
+ */
1451
+ async requestPermission() {
1452
+ if (!("Notification" in window)) {
1453
+ return "denied";
1454
+ }
1455
+ if (Notification.permission === "granted") {
1456
+ return "granted";
1457
+ }
1458
+ if (Notification.permission === "denied") {
1459
+ return "denied";
1460
+ }
1461
+ return await Notification.requestPermission();
1462
+ }
1463
+ /**
1464
+ * Subscribe to push notifications
1465
+ */
1466
+ async subscribeToPush() {
1467
+ if (!this.serviceWorkerRegistration || !this.vapidPublicKey) {
1468
+ return null;
1469
+ }
1470
+ try {
1471
+ let subscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
1472
+ if (!subscription) {
1473
+ const applicationServerKey = this.urlBase64ToUint8Array(this.vapidPublicKey);
1474
+ subscription = await this.serviceWorkerRegistration.pushManager.subscribe({
1475
+ userVisibleOnly: true,
1476
+ applicationServerKey: applicationServerKey.buffer
1477
+ });
1478
+ }
1479
+ return subscription;
1480
+ } catch (error) {
1481
+ this.log(`Push subscription failed: ${error}`, "error");
1482
+ return null;
1483
+ }
1484
+ }
1485
+ /**
1486
+ * Convert base64 VAPID key to Uint8Array
1487
+ */
1488
+ urlBase64ToUint8Array(base64String) {
1489
+ const padding = "=".repeat((4 - base64String.length % 4) % 4);
1490
+ const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
1491
+ const rawData = atob(base64);
1492
+ const outputArray = new Uint8Array(rawData.length);
1493
+ for (let i = 0; i < rawData.length; ++i) {
1494
+ outputArray[i] = rawData.charCodeAt(i);
1495
+ }
1496
+ return outputArray;
1497
+ }
1498
+ /**
1499
+ * Log message if debug is enabled
1500
+ */
1501
+ log(message, level = "log") {
1502
+ if (!this.debug) return;
1503
+ const prefix = "[CloudSignal PWA Push]";
1504
+ console[level](`${prefix} ${message}`);
1505
+ }
1506
+ };
1507
+
1508
+ // src/HeartbeatManager.ts
1509
+ var HeartbeatManager = class {
1510
+ constructor(options) {
1511
+ this.registrationId = null;
1512
+ this.intervalId = null;
1513
+ this.isRunning = false;
1514
+ this.visibilityHandler = null;
1515
+ this.serviceUrl = options.serviceUrl;
1516
+ this.organizationId = options.organizationId;
1517
+ this.organizationSecret = options.organizationSecret;
1518
+ this.debug = options.debug ?? false;
1519
+ this.onHeartbeatSent = options.onHeartbeatSent;
1520
+ this.onHeartbeatError = options.onHeartbeatError;
1521
+ this.config = {
1522
+ enabled: options.config?.enabled ?? true,
1523
+ interval: options.config?.interval ?? 3e4,
1524
+ // 30 seconds
1525
+ autoStart: options.config?.autoStart ?? true,
1526
+ stopOnHidden: options.config?.stopOnHidden ?? true
1527
+ };
1528
+ }
1529
+ /**
1530
+ * Set the registration ID for heartbeat requests
1531
+ */
1532
+ setRegistrationId(registrationId) {
1533
+ this.registrationId = registrationId;
1534
+ if (this.config.autoStart && !this.isRunning && this.config.enabled) {
1535
+ this.start();
1536
+ }
1537
+ }
1538
+ /**
1539
+ * Start sending heartbeats
1540
+ */
1541
+ start() {
1542
+ if (this.isRunning) {
1543
+ this.log("Heartbeat already running");
1544
+ return;
1545
+ }
1546
+ if (!this.registrationId || !isValidUUID(this.registrationId)) {
1547
+ this.log("Cannot start heartbeat: no valid registration ID", "warn");
1548
+ return;
1549
+ }
1550
+ if (!this.config.enabled) {
1551
+ this.log("Heartbeat is disabled");
1552
+ return;
1553
+ }
1554
+ this.isRunning = true;
1555
+ this.log(`Starting heartbeat with interval ${this.config.interval}ms`);
1556
+ this.sendHeartbeat();
1557
+ this.intervalId = setInterval(() => {
1558
+ this.sendHeartbeat();
1559
+ }, this.config.interval);
1560
+ if (this.config.stopOnHidden) {
1561
+ this.setupVisibilityHandler();
1562
+ }
1563
+ }
1564
+ /**
1565
+ * Stop sending heartbeats
1566
+ */
1567
+ stop() {
1568
+ if (!this.isRunning) {
1569
+ return;
1570
+ }
1571
+ this.isRunning = false;
1572
+ if (this.intervalId) {
1573
+ clearInterval(this.intervalId);
1574
+ this.intervalId = null;
1575
+ }
1576
+ if (this.visibilityHandler) {
1577
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
1578
+ this.visibilityHandler = null;
1579
+ }
1580
+ this.log("Heartbeat stopped");
1581
+ }
1582
+ /**
1583
+ * Check if heartbeat is running
1584
+ */
1585
+ isHeartbeatRunning() {
1586
+ return this.isRunning;
1587
+ }
1588
+ /**
1589
+ * Send a single heartbeat
1590
+ */
1591
+ async sendHeartbeat() {
1592
+ if (!this.registrationId || !isValidUUID(this.registrationId)) {
1593
+ this.log("Cannot send heartbeat: no valid registration ID", "warn");
1594
+ return false;
1595
+ }
1596
+ try {
1597
+ const url = `${this.serviceUrl}/api/v1/registration/heartbeat/${this.registrationId}`;
1598
+ const response = await makeAuthenticatedRequest(
1599
+ this.organizationId,
1600
+ this.organizationSecret,
1601
+ "POST",
1602
+ url
1603
+ );
1604
+ if (!response.ok) {
1605
+ throw new Error(`Heartbeat failed: ${response.status}`);
1606
+ }
1607
+ this.log("Heartbeat sent successfully");
1608
+ this.onHeartbeatSent?.();
1609
+ return true;
1610
+ } catch (error) {
1611
+ const err = error instanceof Error ? error : new Error(String(error));
1612
+ this.log(`Heartbeat failed: ${err.message}`, "error");
1613
+ this.onHeartbeatError?.(err);
1614
+ return false;
1615
+ }
1616
+ }
1617
+ /**
1618
+ * Update heartbeat configuration
1619
+ */
1620
+ updateConfig(config) {
1621
+ const wasRunning = this.isRunning;
1622
+ if (wasRunning) {
1623
+ this.stop();
1624
+ }
1625
+ this.config = {
1626
+ ...this.config,
1627
+ ...config
1628
+ };
1629
+ if (wasRunning && this.config.enabled) {
1630
+ this.start();
1631
+ }
1632
+ }
1633
+ /**
1634
+ * Set up visibility change handler
1635
+ */
1636
+ setupVisibilityHandler() {
1637
+ this.visibilityHandler = () => {
1638
+ if (document.hidden) {
1639
+ if (this.intervalId) {
1640
+ clearInterval(this.intervalId);
1641
+ this.intervalId = null;
1642
+ }
1643
+ this.log("Heartbeat paused (page hidden)");
1644
+ } else {
1645
+ if (this.isRunning && !this.intervalId) {
1646
+ this.sendHeartbeat();
1647
+ this.intervalId = setInterval(() => {
1648
+ this.sendHeartbeat();
1649
+ }, this.config.interval);
1650
+ this.log("Heartbeat resumed (page visible)");
1651
+ }
1652
+ }
1653
+ };
1654
+ document.addEventListener("visibilitychange", this.visibilityHandler);
1655
+ }
1656
+ /**
1657
+ * Log message if debug is enabled
1658
+ */
1659
+ log(message, level = "log") {
1660
+ if (!this.debug) return;
1661
+ const prefix = "[CloudSignal PWA Heartbeat]";
1662
+ console[level](`${prefix} ${message}`);
1663
+ }
1664
+ };
1665
+
1666
+ // src/CloudSignalPWA.ts
1667
+ var DEFAULT_SERVICE_URL = "https://pwa.cloudsignal.app";
1668
+ var SDK_VERSION = "1.0.0";
1669
+ var CloudSignalPWA = class {
1670
+ constructor(config) {
1671
+ this.initialized = false;
1672
+ this.serviceConfig = null;
1673
+ // Event emitter
1674
+ this.eventHandlers = /* @__PURE__ */ new Map();
1675
+ this.config = config;
1676
+ this.serviceUrl = config.serviceUrl || DEFAULT_SERVICE_URL;
1677
+ this.debug = config.debug ?? false;
1678
+ this.deviceDetector = new DeviceDetector();
1679
+ this.serviceWorkerManager = new ServiceWorkerManager({
1680
+ config: config.serviceWorker,
1681
+ debug: this.debug,
1682
+ onRegistered: (reg) => this.emit("sw:registered", { registration: reg }),
1683
+ onUpdated: (reg) => this.emit("sw:updated", { registration: reg }),
1684
+ onError: (err) => this.emit("sw:error", { error: err })
1685
+ });
1686
+ this.installationManager = new InstallationManager({
1687
+ debug: this.debug,
1688
+ onInstallAvailable: (event) => this.emit("install:available", { platforms: event.platforms }),
1689
+ onInstallAccepted: (result) => this.emit("install:accepted", result),
1690
+ onInstallDismissed: (result) => this.emit("install:dismissed", result),
1691
+ onInstalled: () => this.emit("install:completed", {})
1692
+ });
1693
+ this.pushNotificationManager = new PushNotificationManager({
1694
+ serviceUrl: this.serviceUrl,
1695
+ organizationId: config.organizationId,
1696
+ organizationSecret: config.organizationSecret,
1697
+ serviceId: config.serviceId,
1698
+ debug: this.debug,
1699
+ onRegistered: (reg) => {
1700
+ this.emit("push:registered", { registrationId: reg.registrationId, endpoint: "" });
1701
+ this.heartbeatManager.setRegistrationId(reg.registrationId);
1702
+ },
1703
+ onUnregistered: () => {
1704
+ this.emit("push:unregistered", {});
1705
+ this.heartbeatManager.stop();
1706
+ },
1707
+ onError: (err) => this.emit("push:error", { error: err }),
1708
+ onPermissionDenied: () => this.emit("permission:denied", { permission: "denied" })
1709
+ });
1710
+ this.heartbeatManager = new HeartbeatManager({
1711
+ serviceUrl: this.serviceUrl,
1712
+ organizationId: config.organizationId,
1713
+ organizationSecret: config.organizationSecret,
1714
+ config: config.heartbeat,
1715
+ debug: this.debug,
1716
+ onHeartbeatSent: () => this.emit("heartbeat:sent", { timestamp: Date.now() }),
1717
+ onHeartbeatError: (err) => this.emit("heartbeat:error", { timestamp: Date.now(), error: err.message })
1718
+ });
1719
+ this.log(`CloudSignal PWA SDK v${SDK_VERSION} initialized`);
1720
+ }
1721
+ // ============================================
1722
+ // Initialization
1723
+ // ============================================
1724
+ /**
1725
+ * Initialize the PWA client
1726
+ * Downloads config, registers service worker, and sets up event listeners
1727
+ */
1728
+ async initialize() {
1729
+ try {
1730
+ this.log("Initializing PWA client...");
1731
+ this.installationManager.initialize();
1732
+ const swReg = await this.serviceWorkerManager.register();
1733
+ if (swReg) {
1734
+ this.pushNotificationManager.setServiceWorkerRegistration(swReg);
1735
+ }
1736
+ const serviceConfig = await this.downloadConfig();
1737
+ this.setupNetworkListeners();
1738
+ this.initialized = true;
1739
+ const result = {
1740
+ success: true,
1741
+ config: serviceConfig || void 0,
1742
+ deviceInfo: this.deviceDetector.getDeviceInfo(),
1743
+ installationState: this.installationManager.getState()
1744
+ };
1745
+ this.log("PWA client initialized successfully");
1746
+ return result;
1747
+ } catch (error) {
1748
+ const err = error instanceof Error ? error : new Error(String(error));
1749
+ this.log(`Initialization failed: ${err.message}`, "error");
1750
+ return {
1751
+ success: false,
1752
+ error: err.message
1753
+ };
1754
+ }
1755
+ }
1756
+ /**
1757
+ * Download PWA service configuration from backend
1758
+ */
1759
+ async downloadConfig() {
1760
+ try {
1761
+ const url = `${this.serviceUrl}/api/v1/config/download`;
1762
+ const response = await makeAuthenticatedRequest(
1763
+ this.config.organizationId,
1764
+ this.config.organizationSecret,
1765
+ "POST",
1766
+ url,
1767
+ { organization_id: this.config.organizationId }
1768
+ );
1769
+ if (!response.ok) {
1770
+ throw new Error(`Config download failed: ${response.status}`);
1771
+ }
1772
+ const config = await response.json();
1773
+ this.serviceConfig = config;
1774
+ if (config.vapid_public_key) {
1775
+ this.pushNotificationManager.setVapidPublicKey(config.vapid_public_key);
1776
+ }
1777
+ if (config.manifest_url) {
1778
+ this.injectManifest(config.manifest_url);
1779
+ }
1780
+ this.emit("config:loaded", { config });
1781
+ this.log("Configuration downloaded successfully");
1782
+ return config;
1783
+ } catch (error) {
1784
+ const err = error instanceof Error ? error : new Error(String(error));
1785
+ this.log(`Config download failed: ${err.message}`, "error");
1786
+ this.emit("config:error", { error: err });
1787
+ return null;
1788
+ }
1789
+ }
1790
+ // ============================================
1791
+ // Installation
1792
+ // ============================================
1793
+ /**
1794
+ * Show the PWA install prompt
1795
+ */
1796
+ async showInstallPrompt() {
1797
+ return this.installationManager.showInstallPrompt();
1798
+ }
1799
+ /**
1800
+ * Get current installation state
1801
+ */
1802
+ getInstallationState() {
1803
+ return this.installationManager.getState();
1804
+ }
1805
+ /**
1806
+ * Check if PWA can be installed
1807
+ */
1808
+ canInstall() {
1809
+ return this.installationManager.canInstall();
1810
+ }
1811
+ /**
1812
+ * Check if PWA is installed
1813
+ */
1814
+ isInstalled() {
1815
+ return this.installationManager.isPWAInstalled();
1816
+ }
1817
+ /**
1818
+ * Get installation steps for current platform
1819
+ */
1820
+ getInstallSteps() {
1821
+ return this.installationManager.getInstallSteps();
1822
+ }
1823
+ // ============================================
1824
+ // Push Notifications
1825
+ // ============================================
1826
+ /**
1827
+ * Register for push notifications
1828
+ */
1829
+ async registerForPush(options) {
1830
+ return this.pushNotificationManager.register(options);
1831
+ }
1832
+ /**
1833
+ * Unregister from push notifications
1834
+ */
1835
+ async unregisterFromPush() {
1836
+ return this.pushNotificationManager.unregister();
1837
+ }
1838
+ /**
1839
+ * Update notification preferences
1840
+ */
1841
+ async updatePreferences(preferences) {
1842
+ return this.pushNotificationManager.updatePreferences(preferences);
1843
+ }
1844
+ /**
1845
+ * Check registration status
1846
+ */
1847
+ async checkRegistrationStatus() {
1848
+ return this.pushNotificationManager.checkStatus();
1849
+ }
1850
+ /**
1851
+ * Get current registration ID
1852
+ */
1853
+ getRegistrationId() {
1854
+ return this.pushNotificationManager.getRegistrationId();
1855
+ }
1856
+ /**
1857
+ * Check if registered for push notifications
1858
+ */
1859
+ isRegistered() {
1860
+ return this.pushNotificationManager.isRegistered();
1861
+ }
1862
+ /**
1863
+ * Request notification permission
1864
+ */
1865
+ async requestPermission() {
1866
+ return this.pushNotificationManager.requestPermission();
1867
+ }
1868
+ // ============================================
1869
+ // Device Information
1870
+ // ============================================
1871
+ /**
1872
+ * Get comprehensive device information
1873
+ */
1874
+ getDeviceInfo() {
1875
+ return this.deviceDetector.getDeviceInfo();
1876
+ }
1877
+ /**
1878
+ * Get PWA capabilities
1879
+ */
1880
+ getCapabilities() {
1881
+ return this.deviceDetector.getCapabilities();
1882
+ }
1883
+ // ============================================
1884
+ // Heartbeat
1885
+ // ============================================
1886
+ /**
1887
+ * Start heartbeat for online status tracking
1888
+ */
1889
+ startHeartbeat() {
1890
+ const regId = this.pushNotificationManager.getRegistrationId();
1891
+ if (regId) {
1892
+ this.heartbeatManager.setRegistrationId(regId);
1893
+ this.heartbeatManager.start();
1894
+ this.emit("heartbeat:started", { timestamp: Date.now() });
1895
+ } else {
1896
+ this.log("Cannot start heartbeat: not registered", "warn");
1897
+ }
1898
+ }
1899
+ /**
1900
+ * Stop heartbeat
1901
+ */
1902
+ stopHeartbeat() {
1903
+ this.heartbeatManager.stop();
1904
+ this.emit("heartbeat:stopped", { timestamp: Date.now() });
1905
+ }
1906
+ // ============================================
1907
+ // Service Worker
1908
+ // ============================================
1909
+ /**
1910
+ * Clear app badge
1911
+ */
1912
+ clearBadge() {
1913
+ this.serviceWorkerManager.clearBadge();
1914
+ }
1915
+ /**
1916
+ * Set app badge count
1917
+ */
1918
+ setBadge(count) {
1919
+ this.serviceWorkerManager.setBadge(count);
1920
+ }
1921
+ /**
1922
+ * Check for service worker updates
1923
+ */
1924
+ async checkForUpdates() {
1925
+ return this.serviceWorkerManager.checkForUpdate();
1926
+ }
1927
+ // ============================================
1928
+ // Events
1929
+ // ============================================
1930
+ /**
1931
+ * Subscribe to an event
1932
+ */
1933
+ on(event, handler) {
1934
+ if (!this.eventHandlers.has(event)) {
1935
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
1936
+ }
1937
+ this.eventHandlers.get(event).add(handler);
1938
+ }
1939
+ /**
1940
+ * Unsubscribe from an event
1941
+ */
1942
+ off(event, handler) {
1943
+ const handlers = this.eventHandlers.get(event);
1944
+ if (handlers) {
1945
+ handlers.delete(handler);
1946
+ }
1947
+ }
1948
+ /**
1949
+ * Emit an event
1950
+ */
1951
+ emit(event, data) {
1952
+ const handlers = this.eventHandlers.get(event);
1953
+ if (handlers) {
1954
+ handlers.forEach((handler) => {
1955
+ try {
1956
+ handler(data);
1957
+ } catch (error) {
1958
+ this.log(`Event handler error for ${event}: ${error}`, "error");
1959
+ }
1960
+ });
1961
+ }
1962
+ }
1963
+ // ============================================
1964
+ // Utility Methods
1965
+ // ============================================
1966
+ /**
1967
+ * Get SDK version
1968
+ */
1969
+ getVersion() {
1970
+ return SDK_VERSION;
1971
+ }
1972
+ /**
1973
+ * Get service configuration
1974
+ */
1975
+ getServiceConfig() {
1976
+ return this.serviceConfig;
1977
+ }
1978
+ /**
1979
+ * Check if client is initialized
1980
+ */
1981
+ isInitialized() {
1982
+ return this.initialized;
1983
+ }
1984
+ /**
1985
+ * Inject PWA manifest link
1986
+ */
1987
+ injectManifest(manifestUrl) {
1988
+ let manifestLink = document.querySelector('link[rel="manifest"]');
1989
+ if (!manifestLink) {
1990
+ manifestLink = document.createElement("link");
1991
+ manifestLink.setAttribute("rel", "manifest");
1992
+ document.head.appendChild(manifestLink);
1993
+ }
1994
+ manifestLink.setAttribute("href", manifestUrl);
1995
+ this.log(`Manifest injected: ${manifestUrl}`);
1996
+ }
1997
+ /**
1998
+ * Set up network status listeners
1999
+ */
2000
+ setupNetworkListeners() {
2001
+ window.addEventListener("online", () => {
2002
+ this.deviceDetector.clearCache();
2003
+ this.emit("network:online", { isOnline: true });
2004
+ });
2005
+ window.addEventListener("offline", () => {
2006
+ this.deviceDetector.clearCache();
2007
+ this.emit("network:offline", { isOnline: false });
2008
+ });
2009
+ }
2010
+ /**
2011
+ * Log message if debug is enabled
2012
+ */
2013
+ log(message, level = "log") {
2014
+ if (!this.debug && level === "log") return;
2015
+ const prefix = "[CloudSignal PWA]";
2016
+ console[level](`${prefix} ${message}`);
2017
+ }
2018
+ };
2019
+ var CloudSignalPWA_default = CloudSignalPWA;
2020
+
2021
+ // src/index.ts
2022
+ var VERSION = "1.0.0";
2023
+
2024
+ export { CloudSignalPWA, DeviceDetector, HeartbeatManager, IndexedDBStorage, InstallationManager, PushNotificationManager, ServiceWorkerManager, VERSION, CloudSignalPWA_default as default, deviceDetector, generateAuthHeaders, generateBrowserFingerprint, generateHMACSignature, generateTrackingId, getRegistrationId, getStorageItem, isValidUUID, makeAuthenticatedRequest, removeRegistrationId, removeStorageItem, setRegistrationId, setStorageItem };
2025
+ //# sourceMappingURL=index.js.map
2026
+ //# sourceMappingURL=index.js.map