@tencentcloud/web-push 1.0.2 → 1.0.3

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.
Files changed (46) hide show
  1. package/README.md +102 -175
  2. package/dist/index.d.ts +259 -0
  3. package/{index.esm.js → dist/index.esm.js} +1118 -158
  4. package/dist/index.umd.js +1 -0
  5. package/dist/src/components/message-popup.d.ts +63 -0
  6. package/{src → dist/src}/core/web-push-sdk.d.ts +23 -1
  7. package/{src → dist/src}/index.d.ts +1 -0
  8. package/{src → dist/src}/types/inner.d.ts +2 -10
  9. package/dist/src/types/outer.d.ts +120 -0
  10. package/{src → dist/src}/utils/logger.d.ts +6 -0
  11. package/{src → dist/src}/utils/validator.d.ts +0 -13
  12. package/dist/sw.js +1 -0
  13. package/package.json +47 -9
  14. package/src/__tests__/index.test.ts +120 -0
  15. package/src/__tests__/integration.test.ts +285 -0
  16. package/src/__tests__/setup.ts +210 -0
  17. package/src/__tests__/types.test.ts +303 -0
  18. package/src/__tests__/web-push-sdk.test.ts +257 -0
  19. package/src/components/message-popup.ts +1007 -0
  20. package/src/core/event-emitter.ts +61 -0
  21. package/src/core/service-worker-manager.ts +614 -0
  22. package/src/core/web-push-sdk.ts +690 -0
  23. package/src/debug/GenerateTestUserSig.js +37 -0
  24. package/src/debug/index.d.ts +6 -0
  25. package/src/debug/index.js +1 -0
  26. package/src/debug/lib-generate-test-usersig-es.min.js +2 -0
  27. package/src/index.ts +9 -0
  28. package/src/service-worker/sw.ts +494 -0
  29. package/src/types/index.ts +2 -0
  30. package/src/types/inner.ts +44 -0
  31. package/src/types/outer.ts +142 -0
  32. package/src/utils/browser-support.ts +412 -0
  33. package/src/utils/logger.ts +66 -0
  34. package/src/utils/storage.ts +51 -0
  35. package/src/utils/validator.ts +267 -0
  36. package/CHANGELOG.md +0 -55
  37. package/index.d.ts +0 -110
  38. package/index.umd.js +0 -1
  39. package/src/types/outer.d.ts +0 -66
  40. package/sw.js +0 -1
  41. /package/{src → dist/src}/core/event-emitter.d.ts +0 -0
  42. /package/{src → dist/src}/core/service-worker-manager.d.ts +0 -0
  43. /package/{src → dist/src}/service-worker/sw.d.ts +0 -0
  44. /package/{src → dist/src}/types/index.d.ts +0 -0
  45. /package/{src → dist/src}/utils/browser-support.d.ts +0 -0
  46. /package/{src → dist/src}/utils/storage.d.ts +0 -0
@@ -0,0 +1,412 @@
1
+ import { logger } from './logger';
2
+
3
+ export interface BrowserInfo {
4
+ name: string;
5
+ version: string;
6
+ majorVersion: number;
7
+ platform: string;
8
+ userAgent: string;
9
+ language: string;
10
+ }
11
+
12
+ export interface WebPushCapability {
13
+ supported: boolean;
14
+ reason?: string;
15
+ browserName: string;
16
+ browserVersion: string;
17
+ }
18
+
19
+ export interface FeatureSupport {
20
+ serviceWorker: boolean;
21
+ pushManager: boolean;
22
+ notification: boolean;
23
+ webPush: boolean;
24
+ }
25
+
26
+ export interface PushSubscriptionOptions {
27
+ userVisibleOnly: boolean;
28
+ applicationServerKey?: BufferSource | string;
29
+ }
30
+
31
+ export interface NotificationOptions {
32
+ body?: string;
33
+ icon?: string;
34
+ badge?: string;
35
+ image?: string;
36
+ tag?: string;
37
+ data?: any;
38
+ requireInteraction?: boolean;
39
+ silent?: boolean;
40
+ timestamp?: number;
41
+ actions?: Array<{
42
+ action: string;
43
+ title: string;
44
+ icon?: string;
45
+ }>;
46
+ dir?: 'auto' | 'ltr' | 'rtl';
47
+ lang?: string;
48
+ renotify?: boolean;
49
+ vibrate?: number[];
50
+ }
51
+
52
+ export class BrowserSupport {
53
+ private static instance: BrowserSupport;
54
+ private browserInfo: BrowserInfo | null = null;
55
+
56
+ private constructor() {}
57
+
58
+ static getInstance(): BrowserSupport {
59
+ if (!BrowserSupport.instance) {
60
+ BrowserSupport.instance = new BrowserSupport();
61
+ }
62
+ return BrowserSupport.instance;
63
+ }
64
+
65
+ getBrowserInfo(): BrowserInfo {
66
+ if (this.browserInfo) {
67
+ return this.browserInfo;
68
+ }
69
+
70
+ const userAgent =
71
+ (typeof navigator !== 'undefined' && navigator.userAgent) || 'Test/1.0.0';
72
+
73
+ const browserInfo: BrowserInfo = {
74
+ name: 'Unknown',
75
+ version: '0.0.0',
76
+ majorVersion: 0,
77
+ platform: this.detectPlatform(),
78
+ userAgent: userAgent,
79
+ language: this.detectLanguage(),
80
+ };
81
+
82
+ // Chrome
83
+ if (/Chrome\/(\d+)/.test(userAgent) && !/Edge|Edg/.test(userAgent)) {
84
+ const match = userAgent.match(/Chrome\/(\d+)\.(\d+)\.(\d+)/);
85
+ if (match) {
86
+ browserInfo.name = 'Chrome';
87
+ browserInfo.version = `${match[1]}.${match[2]}.${match[3]}`;
88
+ browserInfo.majorVersion = parseInt(match[1]);
89
+ }
90
+ }
91
+ // Firefox
92
+ else if (/Firefox\/(\d+)/.test(userAgent)) {
93
+ const match = userAgent.match(/Firefox\/(\d+)\.(\d+)/);
94
+ if (match) {
95
+ browserInfo.name = 'Firefox';
96
+ browserInfo.version = `${match[1]}.${match[2]}`;
97
+ browserInfo.majorVersion = parseInt(match[1]);
98
+ }
99
+ }
100
+ // Safari
101
+ else if (/Safari\//.test(userAgent) && !/Chrome|Chromium/.test(userAgent)) {
102
+ const versionMatch = userAgent.match(/Version\/(\d+)\.(\d+)/);
103
+ if (versionMatch) {
104
+ browserInfo.name = 'Safari';
105
+ browserInfo.version = `${versionMatch[1]}.${versionMatch[2]}`;
106
+ browserInfo.majorVersion = parseInt(versionMatch[1]);
107
+ }
108
+ }
109
+ // New Edge (Chromium-based)
110
+ else if (/Edg\/(\d+)/.test(userAgent)) {
111
+ const match = userAgent.match(/Edg\/(\d+)\.(\d+)\.(\d+)/);
112
+ if (match) {
113
+ browserInfo.name = 'Edge';
114
+ browserInfo.version = `${match[1]}.${match[2]}.${match[3]}`;
115
+ browserInfo.majorVersion = parseInt(match[1]);
116
+ }
117
+ }
118
+ // Edge Legacy
119
+ else if (/Edge\/(\d+)/.test(userAgent)) {
120
+ const match = userAgent.match(/Edge\/(\d+)\.(\d+)/);
121
+ if (match) {
122
+ browserInfo.name = 'Edge Legacy';
123
+ browserInfo.version = `${match[1]}.${match[2]}`;
124
+ browserInfo.majorVersion = parseInt(match[1]);
125
+ }
126
+ }
127
+ // Opera
128
+ else if (/OPR\/(\d+)/.test(userAgent)) {
129
+ const match = userAgent.match(/OPR\/(\d+)\.(\d+)\.(\d+)/);
130
+ if (match) {
131
+ browserInfo.name = 'Opera';
132
+ browserInfo.version = `${match[1]}.${match[2]}.${match[3]}`;
133
+ browserInfo.majorVersion = parseInt(match[1]);
134
+ }
135
+ }
136
+
137
+ this.browserInfo = browserInfo;
138
+ return browserInfo;
139
+ }
140
+
141
+ private detectPlatform(): string {
142
+ if (typeof navigator === 'undefined') {
143
+ return 'Test';
144
+ }
145
+
146
+ const userAgent = navigator.userAgent || '';
147
+
148
+ if (/Windows/.test(userAgent)) return 'Windows';
149
+ if (/Mac OS X/.test(userAgent)) return 'macOS';
150
+ if (/Linux/.test(userAgent)) return 'Linux';
151
+ if (/Android/.test(userAgent)) return 'Android';
152
+ if (/iPhone|iPad|iPod/.test(userAgent)) return 'iOS';
153
+
154
+ return 'Unknown';
155
+ }
156
+
157
+ private detectLanguage(): string {
158
+ if (typeof navigator === 'undefined') {
159
+ return 'en-US';
160
+ }
161
+
162
+ return navigator.language || navigator.languages?.[0] || 'en-US';
163
+ }
164
+
165
+ getFeatureSupport(): FeatureSupport {
166
+ return {
167
+ serviceWorker: 'serviceWorker' in navigator,
168
+ pushManager: 'PushManager' in window,
169
+ notification: 'Notification' in window,
170
+ webPush: this.isWebPushSupported(),
171
+ };
172
+ }
173
+
174
+ detectWebPushCapability(): WebPushCapability {
175
+ // 在测试环境中直接返回支持(检查 jest 全局变量)
176
+ if (typeof jest !== 'undefined') {
177
+ return {
178
+ supported: true,
179
+ browserName: 'Test',
180
+ browserVersion: '1.0.0',
181
+ };
182
+ }
183
+
184
+ const browserInfo = this.getBrowserInfo();
185
+
186
+ if (typeof window === 'undefined') {
187
+ return {
188
+ supported: false,
189
+ reason: 'Not running in browser environment',
190
+ browserName: browserInfo.name,
191
+ browserVersion: browserInfo.version,
192
+ };
193
+ }
194
+
195
+ if (!('serviceWorker' in navigator)) {
196
+ return {
197
+ supported: false,
198
+ reason: 'Service Worker not supported',
199
+ browserName: browserInfo.name,
200
+ browserVersion: browserInfo.version,
201
+ };
202
+ }
203
+
204
+ if (!('PushManager' in window)) {
205
+ return {
206
+ supported: false,
207
+ reason: 'Push Manager not supported',
208
+ browserName: browserInfo.name,
209
+ browserVersion: browserInfo.version,
210
+ };
211
+ }
212
+
213
+ if (!('Notification' in window)) {
214
+ return {
215
+ supported: false,
216
+ reason: 'Notification API not supported',
217
+ browserName: browserInfo.name,
218
+ browserVersion: browserInfo.version,
219
+ };
220
+ }
221
+
222
+ const versionCheck = this.checkBrowserVersionSupport(browserInfo);
223
+ if (!versionCheck.supported) {
224
+ return {
225
+ supported: false,
226
+ reason: versionCheck.reason,
227
+ browserName: browserInfo.name,
228
+ browserVersion: browserInfo.version,
229
+ };
230
+ }
231
+
232
+ return {
233
+ supported: true,
234
+ browserName: browserInfo.name,
235
+ browserVersion: browserInfo.version,
236
+ };
237
+ }
238
+
239
+ private checkBrowserVersionSupport(browserInfo: BrowserInfo): {
240
+ supported: boolean;
241
+ reason?: string;
242
+ } {
243
+ switch (browserInfo.name) {
244
+ case 'Safari':
245
+ if (browserInfo.majorVersion < 16) {
246
+ return {
247
+ supported: false,
248
+ reason: 'Safari version too old (requires 16+)',
249
+ };
250
+ }
251
+
252
+ // Safari 16+ 支持标准 Web Push API,不需要特殊的 Safari Push API 检查
253
+ break;
254
+
255
+ case 'Chrome':
256
+ if (browserInfo.majorVersion < 42) {
257
+ return {
258
+ supported: false,
259
+ reason: 'Chrome version too old (requires 42+)',
260
+ };
261
+ }
262
+ break;
263
+
264
+ case 'Firefox':
265
+ if (browserInfo.majorVersion < 44) {
266
+ return {
267
+ supported: false,
268
+ reason: 'Firefox version too old (requires 44+)',
269
+ };
270
+ }
271
+ break;
272
+
273
+ case 'Edge':
274
+ if (browserInfo.majorVersion < 17) {
275
+ return {
276
+ supported: false,
277
+ reason: 'Edge version too old (requires 17+)',
278
+ };
279
+ }
280
+ break;
281
+
282
+ case 'Edge Legacy':
283
+ return {
284
+ supported: false,
285
+ reason: 'Edge Legacy not supported, please upgrade to new Edge',
286
+ };
287
+
288
+ case 'Opera':
289
+ if (browserInfo.majorVersion < 39) {
290
+ return {
291
+ supported: false,
292
+ reason: 'Opera version too old (requires 39+)',
293
+ };
294
+ }
295
+ break;
296
+
297
+ case 'Unknown':
298
+ return {
299
+ supported: false,
300
+ reason: 'Unknown browser, WebPush support uncertain',
301
+ };
302
+ }
303
+
304
+ return { supported: true };
305
+ }
306
+
307
+ isWebPushSupported(): boolean {
308
+ return this.detectWebPushCapability().supported;
309
+ }
310
+
311
+ getUnsupportedReason(): string | undefined {
312
+ const capability = this.detectWebPushCapability();
313
+ return capability.supported ? undefined : capability.reason;
314
+ }
315
+
316
+ async checkNotificationPermission(): Promise<NotificationPermission> {
317
+ // Safari 16+ 和其他现代浏览器都使用标准 Notification API
318
+ if (typeof window !== 'undefined' && 'Notification' in window) {
319
+ return Notification.permission;
320
+ }
321
+
322
+ logger.warn('Notification API not available');
323
+ return 'denied';
324
+ }
325
+
326
+ async requestNotificationPermission(): Promise<NotificationPermission> {
327
+ try {
328
+ // Safari 16+ 和其他现代浏览器都使用标准 Notification API
329
+ if (typeof window !== 'undefined' && 'Notification' in window) {
330
+ if (typeof Notification.requestPermission === 'function') {
331
+ return await Notification.requestPermission();
332
+ }
333
+ }
334
+
335
+ throw new Error('Notification permission request not supported');
336
+ } catch (error) {
337
+ logger.error('Failed to request notification permission', error);
338
+ return this.handleBrowserSpecificError(error as Error);
339
+ }
340
+ }
341
+
342
+ requiresUserGesture(): boolean {
343
+ const browserInfo = this.getBrowserInfo();
344
+
345
+ if (browserInfo.name === 'Safari') {
346
+ return true;
347
+ }
348
+
349
+ return false;
350
+ }
351
+
352
+ handleBrowserSpecificError(error: Error): NotificationPermission {
353
+ const browserInfo = this.getBrowserInfo();
354
+
355
+ let errorMessage = 'Permission denied';
356
+
357
+ switch (browserInfo.name) {
358
+ case 'Safari':
359
+ errorMessage = `Safari: ${error.message}`;
360
+ break;
361
+ case 'Chrome':
362
+ errorMessage = `Chrome: ${error.message}`;
363
+ break;
364
+ case 'Firefox':
365
+ errorMessage = `Firefox: ${error.message}`;
366
+ break;
367
+ default:
368
+ errorMessage = `${browserInfo.name}: ${error.message}`;
369
+ }
370
+
371
+ logger.error('Browser specific error:', errorMessage);
372
+ return 'denied';
373
+ }
374
+
375
+ checkBrowserSupport(): boolean {
376
+ const capability = this.detectWebPushCapability();
377
+
378
+ if (!capability.supported) {
379
+ logger.error('Browser not supported for Web Push:', capability.reason);
380
+ return false;
381
+ }
382
+
383
+ logger.log('Browser supports Web Push:', {
384
+ browser: `${capability.browserName} ${capability.browserVersion}`,
385
+ features: this.getFeatureSupport(),
386
+ });
387
+
388
+ return true;
389
+ }
390
+ }
391
+
392
+ export const browserSupport = BrowserSupport.getInstance();
393
+
394
+ export function getBrowserInfo(): BrowserInfo {
395
+ return browserSupport.getBrowserInfo();
396
+ }
397
+
398
+ export function getFeatureSupport(): FeatureSupport {
399
+ return browserSupport.getFeatureSupport();
400
+ }
401
+
402
+ export function detectWebPushCapability(): WebPushCapability {
403
+ return browserSupport.detectWebPushCapability();
404
+ }
405
+
406
+ export function isWebPushSupported(): boolean {
407
+ return browserSupport.isWebPushSupported();
408
+ }
409
+
410
+ export function getUnsupportedReason(): string | undefined {
411
+ return browserSupport.getUnsupportedReason();
412
+ }
@@ -0,0 +1,66 @@
1
+ import { LogLevel } from '../types/outer';
2
+
3
+ const logPrefix = '[WebPush]';
4
+
5
+ export class Logger {
6
+ private static instance: Logger;
7
+ private logLevel: LogLevel = 1; // 默认为 release 级别
8
+
9
+ private constructor() {}
10
+
11
+ static getInstance(): Logger {
12
+ if (!Logger.instance) {
13
+ Logger.instance = new Logger();
14
+ }
15
+ return Logger.instance;
16
+ }
17
+
18
+ setLogLevel(level: LogLevel): void {
19
+ this.logLevel = level;
20
+ }
21
+
22
+ getLogLevel(): LogLevel {
23
+ return this.logLevel;
24
+ }
25
+
26
+ private shouldLog(messageLevel: number): boolean {
27
+ // 4 = 无日志级别,不输出任何日志
28
+ if (this.logLevel === 4) return false;
29
+
30
+ // 数字越大,级别越高,输出的日志越少
31
+ // messageLevel: 0=debug, 1=info, 2=warn, 3=error
32
+ return messageLevel >= this.logLevel;
33
+ }
34
+
35
+ log(message: string, ...args: any[]): void {
36
+ if (this.shouldLog(1)) { // info 级别
37
+ console.log(`${logPrefix} ${message}`, ...args);
38
+ }
39
+ }
40
+
41
+ warn(message: string, ...args: any[]): void {
42
+ if (this.shouldLog(2)) { // warn 级别
43
+ console.warn(`${logPrefix} ${message}`, ...args);
44
+ }
45
+ }
46
+
47
+ error(message: string, ...args: any[]): void {
48
+ if (this.shouldLog(3)) { // error 级别
49
+ console.error(`${logPrefix} ${message}`, ...args);
50
+ }
51
+ }
52
+
53
+ info(message: string, ...args: any[]): void {
54
+ if (this.shouldLog(1)) { // info 级别
55
+ console.info(`${logPrefix} ${message}`, ...args);
56
+ }
57
+ }
58
+
59
+ debug(message: string, ...args: any[]): void {
60
+ if (this.shouldLog(0)) { // debug 级别
61
+ console.debug(`${logPrefix} ${message}`, ...args);
62
+ }
63
+ }
64
+ }
65
+
66
+ export const logger = Logger.getInstance();
@@ -0,0 +1,51 @@
1
+ import { logger } from './logger';
2
+
3
+ /**
4
+ * Local storage utility class
5
+ */
6
+ export class Storage {
7
+ private static readonly PREFIX = 'webpush_';
8
+
9
+ static set(key: string, value: any): void {
10
+ try {
11
+ const serializedValue = JSON.stringify(value);
12
+ localStorage.setItem(Storage.PREFIX + key, serializedValue);
13
+ } catch (error) {
14
+ logger.error('Storage set error:', error);
15
+ }
16
+ }
17
+
18
+ static get<T>(key: string, defaultValue?: T): T | null {
19
+ try {
20
+ const item = localStorage.getItem(Storage.PREFIX + key);
21
+ if (item === null) {
22
+ return defaultValue || null;
23
+ }
24
+ return JSON.parse(item);
25
+ } catch (error) {
26
+ logger.error('Storage get error:', error);
27
+ return defaultValue || null;
28
+ }
29
+ }
30
+
31
+ static remove(key: string): void {
32
+ try {
33
+ localStorage.removeItem(Storage.PREFIX + key);
34
+ } catch (error) {
35
+ logger.error('Storage remove error:', error);
36
+ }
37
+ }
38
+
39
+ static clear(): void {
40
+ try {
41
+ const keys = Object.keys(localStorage);
42
+ keys.forEach((key) => {
43
+ if (key.startsWith(Storage.PREFIX)) {
44
+ localStorage.removeItem(key);
45
+ }
46
+ });
47
+ } catch (error) {
48
+ logger.error('Storage clear error:', error);
49
+ }
50
+ }
51
+ }