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