@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/CHANGELOG.md +42 -0
- package/README.md +383 -0
- package/dist/index.cjs +2051 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +1197 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.global.js +9 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +2026 -0
- package/dist/index.js.map +1 -0
- package/dist/service-worker.js +291 -0
- package/dist/service-worker.js.map +1 -0
- package/package.json +72 -0
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
|