@quantabit/push-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,2142 @@
1
+ 'use strict';
2
+
3
+ var sdkConfig = require('@quantabit/sdk-config');
4
+ var React = require('react');
5
+
6
+ /** 浏览器原生 Web Push (Notification API) */
7
+ class WebPushAdapter {
8
+ constructor(config = {}) {
9
+ this.name = 'webpush';
10
+ this._cbs = {};
11
+ this.swPath = config.serviceWorkerPath || '/sw.js';
12
+ }
13
+ onMessage(cb) {
14
+ this._cbs.message = cb;
15
+ }
16
+ onConnect(cb) {
17
+ this._cbs.connect = cb;
18
+ }
19
+ onDisconnect(cb) {
20
+ this._cbs.disconnect = cb;
21
+ }
22
+ onError(cb) {
23
+ this._cbs.error = cb;
24
+ }
25
+ async connect() {
26
+ if (typeof window === 'undefined') return;
27
+ const perm = await Notification.requestPermission();
28
+ if (perm !== 'granted') {
29
+ this._cbs.error?.(new Error('Permission denied'));
30
+ return;
31
+ }
32
+ if ('serviceWorker' in navigator) {
33
+ try {
34
+ await navigator.serviceWorker.register(this.swPath);
35
+ navigator.serviceWorker.addEventListener('message', e => this._cbs.message?.(e.data));
36
+ } catch (e) {
37
+ this._cbs.error?.(e);
38
+ }
39
+ }
40
+ this._cbs.connect?.();
41
+ }
42
+ async disconnect() {
43
+ this._cbs.disconnect?.();
44
+ }
45
+ async subscribe(topic) {/* Web Push subscription via PushManager */}
46
+ async send(msg) {
47
+ if (typeof Notification !== 'undefined') new Notification(msg.title || 'Notification', {
48
+ body: msg.body || msg,
49
+ icon: msg.icon
50
+ });
51
+ }
52
+ }
53
+
54
+ /** WebSocket 自研推送 — 最常用于自建 IM */
55
+ class WebSocketAdapter {
56
+ constructor(config = {}) {
57
+ this.name = 'websocket';
58
+ if (!config.url) throw new Error('[WebSocketAdapter] config.url is required');
59
+ this.url = config.url;
60
+ this.protocols = config.protocols;
61
+ this._ws = null;
62
+ this._cbs = {};
63
+ this._pingInterval = config.pingInterval || 30000;
64
+ this._pingTimer = null;
65
+ }
66
+ onMessage(cb) {
67
+ this._cbs.message = cb;
68
+ }
69
+ onConnect(cb) {
70
+ this._cbs.connect = cb;
71
+ }
72
+ onDisconnect(cb) {
73
+ this._cbs.disconnect = cb;
74
+ }
75
+ onError(cb) {
76
+ this._cbs.error = cb;
77
+ }
78
+ async connect() {
79
+ return new Promise((resolve, reject) => {
80
+ this._ws = new WebSocket(this.url, this.protocols);
81
+ this._ws.onopen = () => {
82
+ this._cbs.connect?.();
83
+ this._startPing();
84
+ resolve();
85
+ };
86
+ this._ws.onmessage = e => {
87
+ try {
88
+ const d = JSON.parse(e.data);
89
+ if (d.type === 'pong') return;
90
+ this._cbs.message?.(d);
91
+ } catch {
92
+ this._cbs.message?.(e.data);
93
+ }
94
+ };
95
+ this._ws.onclose = () => {
96
+ this._stopPing();
97
+ this._cbs.disconnect?.();
98
+ };
99
+ this._ws.onerror = e => {
100
+ this._cbs.error?.(e);
101
+ reject(e);
102
+ };
103
+ });
104
+ }
105
+ async disconnect() {
106
+ this._stopPing();
107
+ this._ws?.close();
108
+ }
109
+ async subscribe(topic) {
110
+ this.send({
111
+ type: 'subscribe',
112
+ topic
113
+ });
114
+ }
115
+ async unsubscribe(topic) {
116
+ this.send({
117
+ type: 'unsubscribe',
118
+ topic
119
+ });
120
+ }
121
+ async send(msg) {
122
+ if (this._ws?.readyState === WebSocket.OPEN) this._ws.send(typeof msg === 'string' ? msg : JSON.stringify(msg));
123
+ }
124
+ _startPing() {
125
+ this._pingTimer = setInterval(() => this.send({
126
+ type: 'ping'
127
+ }), this._pingInterval);
128
+ }
129
+ _stopPing() {
130
+ clearInterval(this._pingTimer);
131
+ }
132
+ }
133
+
134
+ /** Server-Sent Events 推送 — 单向实时流 */
135
+ class SSEAdapter {
136
+ constructor(config = {}) {
137
+ this.name = 'sse';
138
+ this.url = config.url || '/api/events';
139
+ this._source = null;
140
+ this._cbs = {};
141
+ }
142
+ onMessage(cb) {
143
+ this._cbs.message = cb;
144
+ }
145
+ onConnect(cb) {
146
+ this._cbs.connect = cb;
147
+ }
148
+ onDisconnect(cb) {
149
+ this._cbs.disconnect = cb;
150
+ }
151
+ onError(cb) {
152
+ this._cbs.error = cb;
153
+ }
154
+ async connect() {
155
+ this._source = new EventSource(this.url);
156
+ this._source.onopen = () => this._cbs.connect?.();
157
+ this._source.onmessage = e => {
158
+ try {
159
+ this._cbs.message?.(JSON.parse(e.data));
160
+ } catch {
161
+ this._cbs.message?.(e.data);
162
+ }
163
+ };
164
+ this._source.onerror = e => {
165
+ this._cbs.error?.(e);
166
+ if (this._source.readyState === EventSource.CLOSED) this._cbs.disconnect?.();
167
+ };
168
+ }
169
+ async disconnect() {
170
+ this._source?.close();
171
+ this._cbs.disconnect?.();
172
+ }
173
+ async subscribe(topic) {/* SSE 通过 URL 参数区分 topic */}
174
+ }
175
+
176
+ /** Firebase Cloud Messaging 适配器 */
177
+ class FCMAdapter {
178
+ constructor(config = {}) {
179
+ this.name = 'fcm';
180
+ this.config = config;
181
+ this._cbs = {};
182
+ }
183
+ onMessage(cb) {
184
+ this._cbs.message = cb;
185
+ }
186
+ onConnect(cb) {
187
+ this._cbs.connect = cb;
188
+ }
189
+ onDisconnect(cb) {
190
+ this._cbs.disconnect = cb;
191
+ }
192
+ onError(cb) {
193
+ this._cbs.error = cb;
194
+ }
195
+ async connect() {
196
+ try {
197
+ // 需要用户自行引入 firebase/messaging
198
+ if (typeof window !== 'undefined' && window.firebase) {
199
+ const messaging = window.firebase.messaging();
200
+ const token = await messaging.getToken({
201
+ vapidKey: this.config.vapidKey
202
+ });
203
+ messaging.onMessage(payload => this._cbs.message?.(payload));
204
+ this._cbs.connect?.();
205
+ return token;
206
+ }
207
+ this._cbs.connect?.();
208
+ } catch (e) {
209
+ this._cbs.error?.(e);
210
+ }
211
+ }
212
+ async disconnect() {
213
+ this._cbs.disconnect?.();
214
+ }
215
+ async subscribe(topic) {/* FCM 通过服务端管理 topic */}
216
+ }
217
+
218
+ /** OneSignal 推送适配器 */
219
+ class OneSignalAdapter {
220
+ constructor(config = {}) {
221
+ this.name = 'onesignal';
222
+ this.appId = config.appId;
223
+ this._cbs = {};
224
+ }
225
+ onMessage(cb) {
226
+ this._cbs.message = cb;
227
+ }
228
+ onConnect(cb) {
229
+ this._cbs.connect = cb;
230
+ }
231
+ onDisconnect(cb) {
232
+ this._cbs.disconnect = cb;
233
+ }
234
+ onError(cb) {
235
+ this._cbs.error = cb;
236
+ }
237
+ async connect() {
238
+ if (typeof window === 'undefined') return;
239
+ try {
240
+ // 加载 OneSignal SDK (如果未加载)
241
+ if (!window.OneSignal) {
242
+ const script = document.createElement('script');
243
+ script.src = 'https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js';
244
+ script.async = true;
245
+ document.head.appendChild(script);
246
+ await new Promise(r => script.onload = r);
247
+ }
248
+ window.OneSignal = window.OneSignal || [];
249
+ window.OneSignal.push(() => {
250
+ window.OneSignal.init({
251
+ appId: this.appId
252
+ });
253
+ window.OneSignal.on('notificationDisplay', n => this._cbs.message?.(n));
254
+ this._cbs.connect?.();
255
+ });
256
+ } catch (e) {
257
+ this._cbs.error?.(e);
258
+ }
259
+ }
260
+ async disconnect() {
261
+ this._cbs.disconnect?.();
262
+ }
263
+ async subscribe() {
264
+ window.OneSignal?.push(() => window.OneSignal.showSliderPrompt());
265
+ }
266
+ }
267
+
268
+ /** 极光推送 JPush Web Push 适配器 */
269
+ class JPushAdapter {
270
+ constructor(config = {}) {
271
+ this.name = 'jpush';
272
+ this.appKey = config.appKey || config.appId;
273
+ this._cbs = {};
274
+ }
275
+ onMessage(cb) {
276
+ this._cbs.message = cb;
277
+ }
278
+ onConnect(cb) {
279
+ this._cbs.connect = cb;
280
+ }
281
+ onDisconnect(cb) {
282
+ this._cbs.disconnect = cb;
283
+ }
284
+ onError(cb) {
285
+ this._cbs.error = cb;
286
+ }
287
+ async connect() {
288
+ if (typeof window === 'undefined') return;
289
+ try {
290
+ // 极光 Web Push SDK
291
+ if (window.JAnalyticsInterface) {
292
+ window.JAnalyticsInterface.init({
293
+ appkey: this.appKey
294
+ });
295
+ }
296
+ this._cbs.connect?.();
297
+ } catch (e) {
298
+ this._cbs.error?.(e);
299
+ }
300
+ }
301
+ async disconnect() {
302
+ this._cbs.disconnect?.();
303
+ }
304
+ async subscribe(alias) {/* JPush 通过 alias/tag 订阅 */}
305
+ async send(msg) {/* JPush 通过服务端 REST API 发送 */}
306
+ }
307
+
308
+ /** Mock 适配器 — 用于开发和测试,模拟推送消息 */
309
+ class MockAdapter {
310
+ constructor(config = {}) {
311
+ this.name = 'mock';
312
+ this._cbs = {};
313
+ this._timer = null;
314
+ this.interval = config.mockInterval || 5000;
315
+ }
316
+ onMessage(cb) {
317
+ this._cbs.message = cb;
318
+ }
319
+ onConnect(cb) {
320
+ this._cbs.connect = cb;
321
+ }
322
+ onDisconnect(cb) {
323
+ this._cbs.disconnect = cb;
324
+ }
325
+ onError(cb) {
326
+ this._cbs.error = cb;
327
+ }
328
+ async connect() {
329
+ this._cbs.connect?.();
330
+ this._timer = setInterval(() => {
331
+ this._cbs.message?.({
332
+ id: Date.now(),
333
+ type: 'mock',
334
+ title: 'Test Notification',
335
+ body: 'This is a mock push message',
336
+ time: new Date().toISOString()
337
+ });
338
+ }, this.interval);
339
+ }
340
+ async disconnect() {
341
+ clearInterval(this._timer);
342
+ this._cbs.disconnect?.();
343
+ }
344
+ async subscribe() {}
345
+ async send(msg) {
346
+ setTimeout(() => this._cbs.message?.({
347
+ ...msg,
348
+ echo: true
349
+ }), 100);
350
+ }
351
+ // 手动触发消息 (测试用)
352
+ emit(msg) {
353
+ this._cbs.message?.(msg);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * PushManager — 统一推送管理器 (隐私合规增强版)
359
+ * 适配器模式:所有推送服务共享同一接口,可随时切换
360
+ *
361
+ * ⚠️ 推送权限请求和订阅需要 functional 级别隐私同意
362
+ * 连接和消息处理不受限制(属于 essential 功能)
363
+ */
364
+
365
+ // 尝试导入 ConsentManager(可选依赖,不强制)
366
+ let consentManager = null;
367
+ try {
368
+ const sdkConfig = require('@quantabit/sdk-config');
369
+ consentManager = sdkConfig.consentManager;
370
+ } catch {
371
+ // sdk-config 不可用时静默降级
372
+ }
373
+ const ADAPTERS = {
374
+ webpush: WebPushAdapter,
375
+ websocket: WebSocketAdapter,
376
+ sse: SSEAdapter,
377
+ fcm: FCMAdapter,
378
+ onesignal: OneSignalAdapter,
379
+ jpush: JPushAdapter,
380
+ mock: MockAdapter
381
+ };
382
+ class PushManager {
383
+ constructor(config = {}) {
384
+ this.config = config;
385
+ this.debug = config.debug || false;
386
+ this._listeners = new Map();
387
+ this._middleware = [];
388
+ this._connected = false;
389
+ this._messageQueue = [];
390
+ this._retryCount = 0;
391
+ this._maxRetries = config.maxRetries || 5;
392
+ this._retryDelay = config.retryDelay || 3000;
393
+ this._initAdapter(config);
394
+ }
395
+ _initAdapter(config) {
396
+ if (typeof config.adapter === 'string') {
397
+ const Cls = ADAPTERS[config.adapter];
398
+ if (!Cls) throw new Error(`[PushManager] 未知适配器: ${config.adapter}, 可选: ${Object.keys(ADAPTERS).join(', ')}`);
399
+ this._adapter = new Cls(config);
400
+ } else if (config.adapter && typeof config.adapter === 'object') {
401
+ this._adapter = config.adapter;
402
+ } else {
403
+ this._adapter = new MockAdapter(config);
404
+ }
405
+ this._adapter.onMessage?.(msg => this._handleMessage(msg));
406
+ this._adapter.onConnect?.(() => {
407
+ this._connected = true;
408
+ this._retryCount = 0;
409
+ this._emit('connect');
410
+ this._flushQueue();
411
+ });
412
+ this._adapter.onDisconnect?.(() => {
413
+ this._connected = false;
414
+ this._emit('disconnect');
415
+ this._autoReconnect();
416
+ });
417
+ this._adapter.onError?.(err => {
418
+ this._emit('error', err);
419
+ });
420
+ }
421
+ async connect() {
422
+ try {
423
+ await this._adapter.connect?.();
424
+ this._connected = true;
425
+ } catch (e) {
426
+ this._autoReconnect();
427
+ }
428
+ return this;
429
+ }
430
+ async disconnect() {
431
+ this._connected = false;
432
+ await this._adapter.disconnect?.();
433
+ this._emit('disconnect');
434
+ return this;
435
+ }
436
+ async subscribe(topic = 'default') {
437
+ await this._adapter.subscribe?.(topic);
438
+ this._emit('subscribe', topic);
439
+ return this;
440
+ }
441
+ async unsubscribe(topic = 'default') {
442
+ await this._adapter.unsubscribe?.(topic);
443
+ return this;
444
+ }
445
+ async send(message) {
446
+ if (!this._connected) {
447
+ this._messageQueue.push(message);
448
+ return this;
449
+ }
450
+ await this._adapter.send?.(message);
451
+ return this;
452
+ }
453
+ onMessage(cb) {
454
+ return this._on('message', cb);
455
+ }
456
+ onConnect(cb) {
457
+ return this._on('connect', cb);
458
+ }
459
+ onDisconnect(cb) {
460
+ return this._on('disconnect', cb);
461
+ }
462
+ onError(cb) {
463
+ return this._on('error', cb);
464
+ }
465
+ use(mw) {
466
+ this._middleware.push(mw);
467
+ return this;
468
+ }
469
+
470
+ /**
471
+ * 请求推送权限 — 需要 functional 隐私同意
472
+ * 如果用户未授予 functional 同意,返回 'consent_required'
473
+ */
474
+ async requestPermission() {
475
+ // 隐私同意检查:推送权限需要 functional 级别同意
476
+ if (consentManager && !consentManager.hasConsent('functional')) {
477
+ if (this.debug) console.log('[PushManager] 推送权限请求被阻止(需要 functional 隐私同意)');
478
+ return 'consent_required';
479
+ }
480
+ if (typeof Notification !== 'undefined') return await Notification.requestPermission();
481
+ return 'unsupported';
482
+ }
483
+ getPermissionStatus() {
484
+ if (consentManager && !consentManager.hasConsent('functional')) return 'consent_required';
485
+ return typeof Notification !== 'undefined' ? Notification.permission : 'unsupported';
486
+ }
487
+ get isConnected() {
488
+ return this._connected;
489
+ }
490
+ get adapterName() {
491
+ return this._adapter?.name || 'unknown';
492
+ }
493
+ _handleMessage(raw) {
494
+ let msg = raw;
495
+ for (const mw of this._middleware) {
496
+ msg = mw(msg);
497
+ if (!msg) return;
498
+ }
499
+ this._emit('message', msg);
500
+ }
501
+ _flushQueue() {
502
+ while (this._messageQueue.length) this.send(this._messageQueue.shift());
503
+ }
504
+ _autoReconnect() {
505
+ if (this._retryCount >= this._maxRetries) {
506
+ this._emit('error', new Error('Max retries'));
507
+ return;
508
+ }
509
+ this._retryCount++;
510
+ setTimeout(() => this.connect(), this._retryDelay * Math.pow(1.5, this._retryCount - 1));
511
+ }
512
+ _on(ev, cb) {
513
+ if (!this._listeners.has(ev)) this._listeners.set(ev, []);
514
+ this._listeners.get(ev).push(cb);
515
+ return () => {
516
+ const a = this._listeners.get(ev);
517
+ a?.splice(a.indexOf(cb), 1);
518
+ };
519
+ }
520
+ _emit(ev, data) {
521
+ this._listeners.get(ev)?.forEach(fn => {
522
+ try {
523
+ fn(data);
524
+ } catch (e) {}
525
+ });
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Push SDK - API 客户端
531
+ * 推送通知系统后端接口封装
532
+ *
533
+ * 使用 BaseApiClient 基类简化代码
534
+ */
535
+
536
+
537
+ /**
538
+ * 推送 API 客户端
539
+ */
540
+ class PushApiClient extends sdkConfig.BaseApiClient {
541
+ constructor(config = {}) {
542
+ super('/push', config);
543
+ }
544
+
545
+ // ============ 设备管理 ============
546
+
547
+ /**
548
+ * 注册设备
549
+ * @param {Object} deviceInfo - 设备信息
550
+ */
551
+ async registerDevice(deviceInfo) {
552
+ return this.post('/devices', deviceInfo);
553
+ }
554
+
555
+ /**
556
+ * 更新设备 Token
557
+ * @param {string} deviceId - 设备 ID
558
+ * @param {string} token - 推送 Token
559
+ */
560
+ async updateDeviceToken(deviceId, token) {
561
+ return this.put(`/devices/${deviceId}/token`, {
562
+ token
563
+ });
564
+ }
565
+
566
+ /**
567
+ * 获取已注册设备
568
+ */
569
+ async getMyDevices() {
570
+ return this.get('/devices/my');
571
+ }
572
+
573
+ /**
574
+ * 注销设备
575
+ * @param {string} deviceId - 设备 ID
576
+ */
577
+ async unregisterDevice(deviceId) {
578
+ return this.delete(`/devices/${deviceId}`);
579
+ }
580
+
581
+ // ============ 推送设置 ============
582
+
583
+ /**
584
+ * 获取推送偏好
585
+ */
586
+ async getPreferences() {
587
+ return this.get('/preferences');
588
+ }
589
+
590
+ /**
591
+ * 更新推送偏好
592
+ * @param {Object} preferences - 偏好设置
593
+ */
594
+ async updatePreferences(preferences) {
595
+ return this.put('/preferences', preferences);
596
+ }
597
+
598
+ /**
599
+ * 获取免打扰设置
600
+ */
601
+ async getQuietHours() {
602
+ return this.get('/quiet-hours');
603
+ }
604
+
605
+ /**
606
+ * 设置免打扰时段
607
+ * @param {Object} settings - 免打扰设置
608
+ */
609
+ async setQuietHours(settings) {
610
+ return this.put('/quiet-hours', settings);
611
+ }
612
+
613
+ // ============ 推送话题 ============
614
+
615
+ /**
616
+ * 订阅话题
617
+ * @param {string} topic - 话题名称
618
+ */
619
+ async subscribeTopic(topic) {
620
+ return this.post(`/topics/${topic}/subscribe`);
621
+ }
622
+
623
+ /**
624
+ * 取消订阅话题
625
+ * @param {string} topic - 话题名称
626
+ */
627
+ async unsubscribeTopic(topic) {
628
+ return this.post(`/topics/${topic}/unsubscribe`);
629
+ }
630
+
631
+ /**
632
+ * 获取已订阅话题
633
+ */
634
+ async getSubscribedTopics() {
635
+ return this.get('/topics/subscribed');
636
+ }
637
+
638
+ /**
639
+ * 获取可用话题
640
+ */
641
+ async getAvailableTopics() {
642
+ return this.get('/topics');
643
+ }
644
+
645
+ // ============ 推送历史 ============
646
+
647
+ /**
648
+ * 获取推送历史
649
+ * @param {Object} params - 查询参数
650
+ */
651
+ async getHistory(params = {}) {
652
+ return this.get('/history', params);
653
+ }
654
+
655
+ /**
656
+ * 标记已读
657
+ * @param {string} pushId - 推送 ID
658
+ */
659
+ async markAsRead(pushId) {
660
+ return this.post(`/history/${pushId}/read`);
661
+ }
662
+
663
+ /**
664
+ * 批量标记已读
665
+ * @param {string[]} pushIds - 推送 ID 列表
666
+ */
667
+ async batchMarkAsRead(pushIds) {
668
+ return this.post('/history/batch-read', {
669
+ push_ids: pushIds
670
+ });
671
+ }
672
+
673
+ // ============ 管理员操作 ============
674
+
675
+ /**
676
+ * 发送推送(管理员)
677
+ * @param {Object} data - 推送数据
678
+ */
679
+ async send(data) {
680
+ return this.post('/send', data);
681
+ }
682
+
683
+ /**
684
+ * 发送给指定用户(管理员)
685
+ * @param {string[]} userIds - 用户 ID 列表
686
+ * @param {Object} message - 消息内容
687
+ */
688
+ async sendToUsers(userIds, message) {
689
+ return this.post('/send/users', {
690
+ user_ids: userIds,
691
+ ...message
692
+ });
693
+ }
694
+
695
+ /**
696
+ * 发送给话题订阅者(管理员)
697
+ * @param {string} topic - 话题
698
+ * @param {Object} message - 消息内容
699
+ */
700
+ async sendToTopic(topic, message) {
701
+ return this.post('/send/topic', {
702
+ topic,
703
+ ...message
704
+ });
705
+ }
706
+
707
+ /**
708
+ * 全量推送(管理员)
709
+ * @param {Object} message - 消息内容
710
+ */
711
+ async broadcast(message) {
712
+ return this.post('/send/broadcast', message);
713
+ }
714
+
715
+ /**
716
+ * 定时推送(管理员)
717
+ * @param {Object} data - 推送数据
718
+ * @param {string} scheduleAt - 发送时间
719
+ */
720
+ async schedule(data, scheduleAt) {
721
+ return this.post('/schedule', {
722
+ ...data,
723
+ schedule_at: scheduleAt
724
+ });
725
+ }
726
+
727
+ /**
728
+ * 取消定时推送(管理员)
729
+ * @param {string} scheduleId - 定时任务 ID
730
+ */
731
+ async cancelSchedule(scheduleId) {
732
+ return this.delete(`/schedule/${scheduleId}`);
733
+ }
734
+
735
+ // ============ 统计 ============
736
+
737
+ /**
738
+ * 获取推送统计(管理员)
739
+ * @param {Object} params - 统计参数
740
+ */
741
+ async getStats(params = {}) {
742
+ return this.get('/stats', params);
743
+ }
744
+
745
+ /**
746
+ * 获取送达率统计(管理员)
747
+ * @param {Object} params - 统计参数
748
+ */
749
+ async getDeliveryStats(params = {}) {
750
+ return this.get('/stats/delivery', params);
751
+ }
752
+
753
+ // ============ 隐私合规 ============
754
+
755
+ /**
756
+ * 清除所有推送数据 — GDPR 第17条
757
+ * 注销所有设备 + 清除订阅 + 清除历史
758
+ */
759
+ async clearAllUserData() {
760
+ const results = {};
761
+ try {
762
+ const devices = await this.getMyDevices();
763
+ for (const d of devices?.data || []) {
764
+ await this.unregisterDevice(d.id).catch(() => {});
765
+ }
766
+ results.devices = 'cleared';
767
+ } catch (e) {
768
+ results.deviceError = e.message;
769
+ }
770
+ try {
771
+ const topics = await this.getSubscribedTopics();
772
+ for (const t of topics?.data || []) {
773
+ await this.unsubscribeTopic(t.topic || t.name).catch(() => {});
774
+ }
775
+ results.topics = 'cleared';
776
+ } catch (e) {
777
+ results.topicError = e.message;
778
+ }
779
+ return {
780
+ cleared: true,
781
+ timestamp: new Date().toISOString(),
782
+ ...results
783
+ };
784
+ }
785
+
786
+ /**
787
+ * 导出推送数据 — GDPR 第20条
788
+ */
789
+ async exportPushData() {
790
+ try {
791
+ const [prefs, topics, history] = await Promise.allSettled([this.getPreferences(), this.getSubscribedTopics(), this.getHistory({
792
+ page_size: 50
793
+ })]);
794
+ return {
795
+ exportDate: new Date().toISOString(),
796
+ format: 'QBit Push Export (GDPR Art. 20)',
797
+ preferences: prefs.status === 'fulfilled' ? prefs.value?.data : null,
798
+ subscribedTopics: topics.status === 'fulfilled' ? topics.value?.data : [],
799
+ recentHistory: history.status === 'fulfilled' ? history.value?.data?.items : []
800
+ };
801
+ } catch (e) {
802
+ return {
803
+ error: e.message,
804
+ exportDate: new Date().toISOString()
805
+ };
806
+ }
807
+ }
808
+
809
+ /**
810
+ * 获取隐私数据声明
811
+ */
812
+ getDataDisclosure() {
813
+ return {
814
+ sdk: '@quantabit/push-sdk',
815
+ privacyLevel: 'functional',
816
+ consentRequired: true,
817
+ collected: [{
818
+ type: 'device_token',
819
+ description: 'Push notification token (APNs/FCM)',
820
+ retention: 'Until unregistered',
821
+ encrypted: true
822
+ }, {
823
+ type: 'device_info',
824
+ description: 'OS, version, app version',
825
+ retention: 'Until unregistered'
826
+ }, {
827
+ type: 'push_preferences',
828
+ description: 'Notification preferences and quiet hours',
829
+ retention: 'Until account deletion'
830
+ }, {
831
+ type: 'push_history',
832
+ description: 'Delivered notification records',
833
+ retention: '30 days'
834
+ }, {
835
+ type: 'topic_subscriptions',
836
+ description: 'Push topic subscriptions',
837
+ retention: 'Until unsubscribed'
838
+ }],
839
+ gdprCapabilities: ['delete', 'export'],
840
+ note: 'Push notifications require explicit user consent (device permission). Users can revoke at OS level.'
841
+ };
842
+ }
843
+ }
844
+
845
+ // 创建默认实例
846
+ const pushApi = new PushApiClient();
847
+
848
+ /**
849
+ * Push SDK - 类型定义
850
+ */
851
+
852
+ // 推送渠道
853
+ const PushChannel = {
854
+ WEB_PUSH: 'web_push',
855
+ FCM: 'fcm',
856
+ // Firebase Cloud Messaging
857
+ APNS: 'apns',
858
+ // Apple Push Notification
859
+ EMAIL: 'email',
860
+ SMS: 'sms',
861
+ WECHAT: 'wechat',
862
+ IN_APP: 'in_app'
863
+ };
864
+
865
+ // 推送状态
866
+ const PushStatus = {
867
+ PENDING: 'pending',
868
+ SENT: 'sent',
869
+ DELIVERED: 'delivered',
870
+ FAILED: 'failed',
871
+ CLICKED: 'clicked',
872
+ DISMISSED: 'dismissed'
873
+ };
874
+
875
+ // 推送优先级
876
+ const PushPriority = {
877
+ LOW: 'low',
878
+ NORMAL: 'normal',
879
+ HIGH: 'high'
880
+ };
881
+
882
+ // 订阅状态
883
+ const SubscriptionStatus = {
884
+ ACTIVE: 'active',
885
+ EXPIRED: 'expired',
886
+ UNSUBSCRIBED: 'unsubscribed',
887
+ DENIED: 'denied'
888
+ };
889
+
890
+ // 权限状态
891
+ const PermissionStatus = {
892
+ GRANTED: 'granted',
893
+ DENIED: 'denied',
894
+ DEFAULT: 'default',
895
+ PROMPT: 'prompt'
896
+ };
897
+ const NotificationStatus = PushStatus;
898
+ const NotificationType = {
899
+ SYSTEM: 'system',
900
+ ANNOUNCEMENT: 'announcement',
901
+ TRANSACTION: 'transaction',
902
+ ACTIVITY: 'activity',
903
+ REMINDER: 'reminder',
904
+ MARKETING: 'marketing'
905
+ };
906
+ const NotificationChannel = PushChannel;
907
+
908
+ /**
909
+ * Push SDK - Web Push 客户端
910
+ * 使用 BaseApiClient 基类,继承统一的配置、Token 管理和错误处理
911
+ */
912
+
913
+
914
+ /**
915
+ * Web Push 客户端
916
+ */
917
+ class WebPushClientClass extends sdkConfig.BaseApiClient {
918
+ constructor(config = {}) {
919
+ super('/push', config);
920
+ this.vapidPublicKey = config.vapidPublicKey || '';
921
+ this.onMessage = config.onMessage;
922
+ this.subscription = null;
923
+ this.registration = null;
924
+ this.permission = PermissionStatus.DEFAULT;
925
+ }
926
+
927
+ /**
928
+ * 初始化
929
+ */
930
+ async init(config = {}) {
931
+ if (config.vapidPublicKey) this.vapidPublicKey = config.vapidPublicKey;
932
+ if (!this.isSupported()) {
933
+ console.warn('Web Push is not supported');
934
+ return false;
935
+ }
936
+
937
+ // 检查权限状态
938
+ this.permission = Notification.permission;
939
+
940
+ // 注册 Service Worker
941
+ try {
942
+ this.registration = await navigator.serviceWorker.register('/sw.js');
943
+ console.log('Service Worker registered');
944
+ } catch (err) {
945
+ console.error('Service Worker registration failed:', err);
946
+ return false;
947
+ }
948
+
949
+ // 检查现有订阅
950
+ this.subscription = await this.registration.pushManager.getSubscription();
951
+
952
+ // 监听消息
953
+ this.setupMessageListener();
954
+ return true;
955
+ }
956
+
957
+ /**
958
+ * 检查是否支持 Web Push
959
+ */
960
+ isSupported() {
961
+ return typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
962
+ }
963
+
964
+ /**
965
+ * 请求权限
966
+ */
967
+ async requestPermission() {
968
+ if (!this.isSupported()) {
969
+ return PermissionStatus.DENIED;
970
+ }
971
+ try {
972
+ const permission = await Notification.requestPermission();
973
+ this.permission = permission;
974
+ if (permission === 'granted') {
975
+ await this.subscribe();
976
+ }
977
+ return permission;
978
+ } catch (err) {
979
+ console.error('Permission request failed:', err);
980
+ this.onError?.(err);
981
+ return PermissionStatus.DENIED;
982
+ }
983
+ }
984
+
985
+ /**
986
+ * 订阅推送
987
+ */
988
+ async subscribe() {
989
+ if (!this.registration) {
990
+ await this.init();
991
+ }
992
+ if (!this.vapidPublicKey) {
993
+ throw new Error('VAPID public key not configured');
994
+ }
995
+ try {
996
+ this.subscription = await this.registration.pushManager.subscribe({
997
+ userVisibleOnly: true,
998
+ applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
999
+ });
1000
+
1001
+ // 将订阅信息发送到服务器
1002
+ await this.saveSubscription(this.subscription);
1003
+ console.log('Push subscription successful');
1004
+ return this.subscription;
1005
+ } catch (err) {
1006
+ console.error('Push subscription failed:', err);
1007
+ this.onError?.(err);
1008
+ throw err;
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * 取消订阅
1014
+ */
1015
+ async unsubscribe() {
1016
+ if (!this.subscription) {
1017
+ return;
1018
+ }
1019
+ try {
1020
+ await this.subscription.unsubscribe();
1021
+ await this.removeSubscription();
1022
+ this.subscription = null;
1023
+ console.log('Push unsubscribed');
1024
+ } catch (err) {
1025
+ console.error('Unsubscribe failed:', err);
1026
+ this.onError?.(err);
1027
+ }
1028
+ }
1029
+
1030
+ /**
1031
+ * 保存订阅到服务器
1032
+ */
1033
+ async saveSubscription(subscription) {
1034
+ return this.post('/subscriptions', {
1035
+ subscription: subscription.toJSON(),
1036
+ channel: 'web_push'
1037
+ });
1038
+ }
1039
+
1040
+ /**
1041
+ * 从服务器移除订阅
1042
+ */
1043
+ async removeSubscription() {
1044
+ return this.delete('/subscriptions/current');
1045
+ }
1046
+
1047
+ /**
1048
+ * 监听消息
1049
+ */
1050
+ setupMessageListener() {
1051
+ if (!('serviceWorker' in navigator)) return;
1052
+ navigator.serviceWorker.addEventListener('message', event => {
1053
+ if (event.data && event.data.type === 'PUSH_RECEIVED') {
1054
+ this.onMessage?.(event.data.payload);
1055
+ }
1056
+ });
1057
+ }
1058
+
1059
+ /**
1060
+ * 获取订阅状态
1061
+ */
1062
+ getSubscriptionStatus() {
1063
+ if (!this.isSupported()) {
1064
+ return SubscriptionStatus.DENIED;
1065
+ }
1066
+ if (this.permission === 'denied') {
1067
+ return SubscriptionStatus.DENIED;
1068
+ }
1069
+ if (this.subscription) {
1070
+ return SubscriptionStatus.ACTIVE;
1071
+ }
1072
+ return SubscriptionStatus.UNSUBSCRIBED;
1073
+ }
1074
+
1075
+ /**
1076
+ * 获取权限状态
1077
+ */
1078
+ getPermissionStatus() {
1079
+ if (!this.isSupported()) {
1080
+ return PermissionStatus.DENIED;
1081
+ }
1082
+ return Notification.permission;
1083
+ }
1084
+
1085
+ /**
1086
+ * 显示本地通知
1087
+ */
1088
+ async showNotification(title, options = {}) {
1089
+ if (this.permission !== 'granted') {
1090
+ return;
1091
+ }
1092
+ if (this.registration) {
1093
+ return this.registration.showNotification(title, {
1094
+ icon: options.icon || '/icon-192.png',
1095
+ badge: options.badge || '/badge-72.png',
1096
+ ...options
1097
+ });
1098
+ }
1099
+
1100
+ // 降级到 Notification API
1101
+ return new Notification(title, options);
1102
+ }
1103
+
1104
+ /**
1105
+ * Base64 转 Uint8Array
1106
+ */
1107
+ urlBase64ToUint8Array(base64String) {
1108
+ const padding = '='.repeat((4 - base64String.length % 4) % 4);
1109
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
1110
+ const rawData = atob(base64);
1111
+ const outputArray = new Uint8Array(rawData.length);
1112
+ for (let i = 0; i < rawData.length; ++i) {
1113
+ outputArray[i] = rawData.charCodeAt(i);
1114
+ }
1115
+ return outputArray;
1116
+ }
1117
+ }
1118
+ const webPushClient = new WebPushClientClass();
1119
+ const WebPushClient = WebPushClientClass;
1120
+
1121
+ /**
1122
+ * Push SDK - 国际化
1123
+ * 消息推送多语言支持
1124
+ */
1125
+
1126
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
1127
+ const messages = {
1128
+ zh: {
1129
+ // 推送
1130
+ push: '推送',
1131
+ notification: '通知',
1132
+ notifications: '通知',
1133
+ pushNotification: '推送通知',
1134
+ // 类型
1135
+ type: '类型',
1136
+ system: '系统通知',
1137
+ marketing: '营销推送',
1138
+ transactional: '交易通知',
1139
+ social: '社交通知',
1140
+ reminder: '提醒',
1141
+ alert: '警报',
1142
+ // 渠道
1143
+ channel: '渠道',
1144
+ webPush: 'Web推送',
1145
+ appPush: 'App推送',
1146
+ email: '邮件',
1147
+ sms: '短信',
1148
+ inApp: '站内信',
1149
+ // 状态
1150
+ status: '状态',
1151
+ pending: '待发送',
1152
+ sent: '已发送',
1153
+ delivered: '已送达',
1154
+ read: '已读',
1155
+ clicked: '已点击',
1156
+ failed: '发送失败',
1157
+ // 权限
1158
+ permission: '权限',
1159
+ granted: '已授权',
1160
+ denied: '已拒绝',
1161
+ default: '未设置',
1162
+ requestPermission: '开启通知权限',
1163
+ permissionRequired: '需要通知权限',
1164
+ // 设置
1165
+ settings: '设置',
1166
+ enabled: '已开启',
1167
+ disabled: '已关闭',
1168
+ enablePush: '开启推送',
1169
+ disablePush: '关闭推送',
1170
+ // 偏好
1171
+ preferences: '偏好设置',
1172
+ muteAll: '全部静音',
1173
+ muteDuration: '静音时长',
1174
+ quietHours: '免打扰时段',
1175
+ // 频率
1176
+ frequency: '频率',
1177
+ realtime: '实时',
1178
+ daily: '每日摘要',
1179
+ weekly: '每周摘要',
1180
+ // 操作
1181
+ send: '发送',
1182
+ schedule: '定时发送',
1183
+ cancel: '取消',
1184
+ resend: '重试',
1185
+ markAsRead: '标记已读',
1186
+ markAllRead: '全部已读',
1187
+ delete: '删除',
1188
+ clear: '清空',
1189
+ // 时间
1190
+ now: '刚刚',
1191
+ minutesAgo: '{n}分钟前',
1192
+ hoursAgo: '{n}小时前',
1193
+ yesterday: '昨天',
1194
+ daysAgo: '{n}天前',
1195
+ // 状态
1196
+ loading: '加载中...',
1197
+ noNotifications: '暂无通知',
1198
+ error: '加载失败',
1199
+ // 标题
1200
+ unread: '未读',
1201
+ all: '全部',
1202
+ newNotification: '新通知'
1203
+ },
1204
+ en: {
1205
+ push: 'Push',
1206
+ notification: 'Notification',
1207
+ notifications: 'Notifications',
1208
+ pushNotification: 'Push Notification',
1209
+ type: 'Type',
1210
+ system: 'System',
1211
+ marketing: 'Marketing',
1212
+ transactional: 'Transactional',
1213
+ social: 'Social',
1214
+ reminder: 'Reminder',
1215
+ alert: 'Alert',
1216
+ channel: 'Channel',
1217
+ webPush: 'Web Push',
1218
+ appPush: 'App Push',
1219
+ email: 'Email',
1220
+ sms: 'SMS',
1221
+ inApp: 'In-App',
1222
+ status: 'Status',
1223
+ pending: 'Pending',
1224
+ sent: 'Sent',
1225
+ delivered: 'Delivered',
1226
+ read: 'Read',
1227
+ clicked: 'Clicked',
1228
+ failed: 'Failed',
1229
+ permission: 'Permission',
1230
+ granted: 'Granted',
1231
+ denied: 'Denied',
1232
+ default: 'Default',
1233
+ requestPermission: 'Enable Notifications',
1234
+ permissionRequired: 'Permission Required',
1235
+ settings: 'Settings',
1236
+ enabled: 'Enabled',
1237
+ disabled: 'Disabled',
1238
+ enablePush: 'Enable Push',
1239
+ disablePush: 'Disable Push',
1240
+ preferences: 'Preferences',
1241
+ muteAll: 'Mute All',
1242
+ muteDuration: 'Mute Duration',
1243
+ quietHours: 'Quiet Hours',
1244
+ frequency: 'Frequency',
1245
+ realtime: 'Realtime',
1246
+ daily: 'Daily Digest',
1247
+ weekly: 'Weekly Digest',
1248
+ send: 'Send',
1249
+ schedule: 'Schedule',
1250
+ cancel: 'Cancel',
1251
+ resend: 'Resend',
1252
+ markAsRead: 'Mark as Read',
1253
+ markAllRead: 'Mark All Read',
1254
+ delete: 'Delete',
1255
+ clear: 'Clear',
1256
+ now: 'Just now',
1257
+ minutesAgo: '{n}m ago',
1258
+ hoursAgo: '{n}h ago',
1259
+ yesterday: 'Yesterday',
1260
+ daysAgo: '{n}d ago',
1261
+ loading: 'Loading...',
1262
+ noNotifications: 'No Notifications',
1263
+ error: 'Error',
1264
+ unread: 'Unread',
1265
+ all: 'All',
1266
+ newNotification: 'New Notification'
1267
+ },
1268
+ ja: {
1269
+ push: 'プッシュ',
1270
+ notification: '通知',
1271
+ notifications: '通知',
1272
+ pushNotification: 'プッシュ通知',
1273
+ type: 'タイプ',
1274
+ system: 'システム',
1275
+ marketing: 'マーケティング',
1276
+ transactional: '取引',
1277
+ social: 'ソーシャル',
1278
+ reminder: 'リマインダー',
1279
+ alert: 'アラート',
1280
+ channel: 'チャネル',
1281
+ webPush: 'Webプッシュ',
1282
+ appPush: 'アプリプッシュ',
1283
+ email: 'メール',
1284
+ sms: 'SMS',
1285
+ inApp: 'アプリ内',
1286
+ status: 'ステータス',
1287
+ pending: '待機中',
1288
+ sent: '送信済み',
1289
+ delivered: '配信済み',
1290
+ read: '既読',
1291
+ clicked: 'クリック済み',
1292
+ failed: '失敗',
1293
+ permission: '権限',
1294
+ granted: '許可済み',
1295
+ denied: '拒否',
1296
+ default: '未設定',
1297
+ requestPermission: '通知を許可',
1298
+ permissionRequired: '権限が必要です',
1299
+ settings: '設定',
1300
+ enabled: 'オン',
1301
+ disabled: 'オフ',
1302
+ enablePush: 'プッシュを有効化',
1303
+ disablePush: 'プッシュを無効化',
1304
+ preferences: '通知設定',
1305
+ muteAll: 'すべてミュート',
1306
+ muteDuration: 'ミュート期間',
1307
+ quietHours: 'おやすみ時間',
1308
+ frequency: '頻度',
1309
+ realtime: 'リアルタイム',
1310
+ daily: '日次まとめ',
1311
+ weekly: '週次まとめ',
1312
+ send: '送信',
1313
+ schedule: '予約送信',
1314
+ cancel: 'キャンセル',
1315
+ resend: '再送信',
1316
+ markAsRead: '既読にする',
1317
+ markAllRead: 'すべて既読',
1318
+ delete: '削除',
1319
+ clear: 'クリア',
1320
+ now: 'たった今',
1321
+ minutesAgo: '{n}分前',
1322
+ hoursAgo: '{n}時間前',
1323
+ yesterday: '昨日',
1324
+ daysAgo: '{n}日前',
1325
+ loading: '読み込み中...',
1326
+ noNotifications: '通知はありません',
1327
+ error: 'エラー',
1328
+ unread: '未読',
1329
+ all: 'すべて',
1330
+ newNotification: '新しい通知'
1331
+ },
1332
+ ko: {
1333
+ push: '푸시',
1334
+ notification: '알림',
1335
+ notifications: '알림',
1336
+ pushNotification: '푸시 알림',
1337
+ type: '유형',
1338
+ system: '시스템',
1339
+ marketing: '마케팅',
1340
+ transactional: '거래',
1341
+ social: '소셜',
1342
+ reminder: '리마인더',
1343
+ alert: '경고',
1344
+ channel: '채널',
1345
+ webPush: '웹 푸시',
1346
+ appPush: '앱 푸시',
1347
+ email: '이메일',
1348
+ sms: 'SMS',
1349
+ inApp: '인앱',
1350
+ status: '상태',
1351
+ pending: '대기 중',
1352
+ sent: '전송됨',
1353
+ delivered: '전달됨',
1354
+ read: '읽음',
1355
+ clicked: '클릭됨',
1356
+ failed: '실패',
1357
+ permission: '권한',
1358
+ granted: '허용됨',
1359
+ denied: '거부됨',
1360
+ default: '미설정',
1361
+ requestPermission: '알림 허용',
1362
+ permissionRequired: '권한이 필요합니다',
1363
+ settings: '설정',
1364
+ enabled: '사용',
1365
+ disabled: '사용 안 함',
1366
+ enablePush: '푸시 사용',
1367
+ disablePush: '푸시 사용 안 함',
1368
+ preferences: '알림 설정',
1369
+ muteAll: '모두 음소거',
1370
+ muteDuration: '음소거 기간',
1371
+ quietHours: '방해 금지 시간',
1372
+ frequency: '빈도',
1373
+ realtime: '실시간',
1374
+ daily: '일일 다이제스트',
1375
+ weekly: '주간 다이제스트',
1376
+ send: '보내기',
1377
+ schedule: '예약',
1378
+ cancel: '취소',
1379
+ resend: '재전송',
1380
+ markAsRead: '읽음으로 표시',
1381
+ markAllRead: '모두 읽음',
1382
+ delete: '삭제',
1383
+ clear: '비우기',
1384
+ now: '방금',
1385
+ minutesAgo: '{n}분 전',
1386
+ hoursAgo: '{n}시간 전',
1387
+ yesterday: '어제',
1388
+ daysAgo: '{n}일 전',
1389
+ loading: '로딩 중...',
1390
+ noNotifications: '알림 없음',
1391
+ error: '오류',
1392
+ unread: '읽지 않음',
1393
+ all: '전체',
1394
+ newNotification: '새 알림'
1395
+ }
1396
+ };
1397
+ let currentLanguage = 'zh';
1398
+ function setLanguage(lang) {
1399
+ if (SUPPORTED_LANGUAGES.includes(lang)) currentLanguage = lang;
1400
+ }
1401
+ function getLanguage() {
1402
+ return currentLanguage;
1403
+ }
1404
+ function t(key, params = {}) {
1405
+ let text = (messages[currentLanguage] || messages.en)[key] || key;
1406
+ Object.keys(params).forEach(k => {
1407
+ text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), params[k]);
1408
+ });
1409
+ return text;
1410
+ }
1411
+
1412
+ function usePush(config = {}) {
1413
+ const mgr = React.useRef(null);
1414
+ const [connected, setConnected] = React.useState(false);
1415
+ const [messages, setMessages] = React.useState([]);
1416
+ const [lastMessage, setLastMessage] = React.useState(null);
1417
+ const [permission, setPermission] = React.useState('default');
1418
+ React.useEffect(() => {
1419
+ mgr.current = new PushManager(config);
1420
+ mgr.current.onConnect(() => setConnected(true));
1421
+ mgr.current.onDisconnect(() => setConnected(false));
1422
+ mgr.current.onMessage(msg => {
1423
+ setLastMessage(msg);
1424
+ setMessages(prev => [msg, ...prev].slice(0, 100));
1425
+ });
1426
+ mgr.current.connect();
1427
+ setPermission(mgr.current.getPermissionStatus());
1428
+ return () => mgr.current?.disconnect();
1429
+ }, []);
1430
+ const send = React.useCallback(msg => mgr.current?.send(msg), []);
1431
+ const subscribe = React.useCallback(topic => mgr.current?.subscribe(topic), []);
1432
+ const requestPermission = React.useCallback(async () => {
1433
+ const p = await mgr.current?.requestPermission();
1434
+ setPermission(p);
1435
+ return p;
1436
+ }, []);
1437
+ const clearMessages = React.useCallback(() => setMessages([]), []);
1438
+ return {
1439
+ connected,
1440
+ messages,
1441
+ lastMessage,
1442
+ send,
1443
+ subscribe,
1444
+ requestPermission,
1445
+ permission,
1446
+ clearMessages,
1447
+ manager: mgr.current
1448
+ };
1449
+ }
1450
+ function usePushPermission() {
1451
+ const supported = typeof window !== 'undefined' && 'Notification' in window;
1452
+ const [permission, setPermission] = React.useState(supported ? window.Notification.permission : 'denied');
1453
+ const requestPermission = React.useCallback(async () => {
1454
+ if (!supported) return 'denied';
1455
+ const result = await window.Notification.requestPermission();
1456
+ setPermission(result);
1457
+ return result;
1458
+ }, [supported]);
1459
+ return {
1460
+ supported,
1461
+ permission,
1462
+ requestPermission
1463
+ };
1464
+ }
1465
+ function useNotifications(options = {}) {
1466
+ const [notifications, setNotifications] = React.useState([]);
1467
+ const [unreadCount, setUnreadCount] = React.useState(0);
1468
+ const [loading, setLoading] = React.useState(false);
1469
+ const [error, setError] = React.useState(null);
1470
+ const refresh = React.useCallback(async () => {
1471
+ setLoading(true);
1472
+ setError(null);
1473
+ try {
1474
+ const [historyResult, statsResult] = await Promise.allSettled([pushApi.getHistory(options), pushApi.getStats({
1475
+ scope: 'unread'
1476
+ })]);
1477
+ const history = historyResult.status === 'fulfilled' ? historyResult.value?.data?.items || historyResult.value?.data || [] : [];
1478
+ setNotifications(Array.isArray(history) ? history : []);
1479
+ if (statsResult.status === 'fulfilled') {
1480
+ setUnreadCount(statsResult.value?.data?.unread_count || statsResult.value?.data?.unread || 0);
1481
+ }
1482
+ return historyResult.status === 'fulfilled' ? historyResult.value : null;
1483
+ } catch (err) {
1484
+ setError(err.message);
1485
+ return null;
1486
+ } finally {
1487
+ setLoading(false);
1488
+ }
1489
+ }, [options.filter, options.limit, options.page]);
1490
+ React.useEffect(() => {
1491
+ refresh();
1492
+ }, [refresh]);
1493
+ const markAsRead = React.useCallback(async pushId => {
1494
+ const result = await pushApi.markAsRead(pushId);
1495
+ await refresh();
1496
+ return result;
1497
+ }, [refresh]);
1498
+ const markAllAsRead = React.useCallback(async () => {
1499
+ const ids = notifications.map(item => item.id).filter(Boolean);
1500
+ if (ids.length === 0) return null;
1501
+ const result = await pushApi.batchMarkAsRead(ids);
1502
+ await refresh();
1503
+ return result;
1504
+ }, [notifications, refresh]);
1505
+ const deleteNotification = React.useCallback(async () => {
1506
+ // Push history deletion is not exposed yet; keep the hook API stable.
1507
+ await refresh();
1508
+ return null;
1509
+ }, [refresh]);
1510
+ const clearAll = React.useCallback(() => {
1511
+ setNotifications([]);
1512
+ setUnreadCount(0);
1513
+ }, []);
1514
+ return {
1515
+ notifications,
1516
+ unreadCount,
1517
+ loading,
1518
+ error,
1519
+ refresh,
1520
+ markAsRead,
1521
+ markAllAsRead,
1522
+ deleteNotification,
1523
+ clearAll
1524
+ };
1525
+ }
1526
+ function useRealtimeNotifications(config = {}) {
1527
+ return usePush(config);
1528
+ }
1529
+ function useNotificationPreferences() {
1530
+ const [preferences, setPreferences] = React.useState({});
1531
+ const [loading, setLoading] = React.useState(false);
1532
+ const [saving, setSaving] = React.useState(false);
1533
+ const [error, setError] = React.useState(null);
1534
+ const refresh = React.useCallback(async () => {
1535
+ setLoading(true);
1536
+ setError(null);
1537
+ try {
1538
+ const result = await pushApi.getPreferences();
1539
+ setPreferences(result.data || {});
1540
+ return result;
1541
+ } catch (err) {
1542
+ setError(err.message);
1543
+ return null;
1544
+ } finally {
1545
+ setLoading(false);
1546
+ }
1547
+ }, []);
1548
+ React.useEffect(() => {
1549
+ refresh();
1550
+ }, [refresh]);
1551
+ const updatePreferences = React.useCallback(async nextPreferences => {
1552
+ setSaving(true);
1553
+ try {
1554
+ const result = await pushApi.updatePreferences(nextPreferences);
1555
+ setPreferences(nextPreferences);
1556
+ return result;
1557
+ } finally {
1558
+ setSaving(false);
1559
+ }
1560
+ }, []);
1561
+ const toggleChannel = React.useCallback(channel => {
1562
+ const next = {
1563
+ ...preferences,
1564
+ channels: {
1565
+ ...(preferences.channels || {}),
1566
+ [channel]: !preferences.channels?.[channel]
1567
+ }
1568
+ };
1569
+ return updatePreferences(next);
1570
+ }, [preferences, updatePreferences]);
1571
+ const toggleType = React.useCallback(type => {
1572
+ const next = {
1573
+ ...preferences,
1574
+ types: {
1575
+ ...(preferences.types || {}),
1576
+ [type]: !preferences.types?.[type]
1577
+ }
1578
+ };
1579
+ return updatePreferences(next);
1580
+ }, [preferences, updatePreferences]);
1581
+ return {
1582
+ preferences,
1583
+ loading,
1584
+ saving,
1585
+ error,
1586
+ refresh,
1587
+ updatePreferences,
1588
+ toggleChannel,
1589
+ toggleType
1590
+ };
1591
+ }
1592
+ function usePushSubscription() {
1593
+ const [topics, setTopics] = React.useState([]);
1594
+ const refresh = React.useCallback(async () => {
1595
+ const result = await pushApi.getSubscribedTopics();
1596
+ setTopics(result.data || []);
1597
+ return result;
1598
+ }, []);
1599
+ React.useEffect(() => {
1600
+ refresh();
1601
+ }, [refresh]);
1602
+ const subscribe = React.useCallback(async topic => {
1603
+ const result = await pushApi.subscribeTopic(topic);
1604
+ await refresh();
1605
+ return result;
1606
+ }, [refresh]);
1607
+ const unsubscribe = React.useCallback(async topic => {
1608
+ const result = await pushApi.unsubscribeTopic(topic);
1609
+ await refresh();
1610
+ return result;
1611
+ }, [refresh]);
1612
+ return {
1613
+ topics,
1614
+ refresh,
1615
+ subscribe,
1616
+ unsubscribe
1617
+ };
1618
+ }
1619
+ function useSendNotification() {
1620
+ const [loading, setLoading] = React.useState(false);
1621
+ const [error, setError] = React.useState(null);
1622
+ const send = React.useCallback(async message => {
1623
+ setLoading(true);
1624
+ setError(null);
1625
+ try {
1626
+ return await pushApi.send(message);
1627
+ } catch (err) {
1628
+ setError(err.message);
1629
+ throw err;
1630
+ } finally {
1631
+ setLoading(false);
1632
+ }
1633
+ }, []);
1634
+ const schedule = React.useCallback(async (message, scheduleAt) => {
1635
+ setLoading(true);
1636
+ setError(null);
1637
+ try {
1638
+ return await pushApi.schedule(message, scheduleAt);
1639
+ } catch (err) {
1640
+ setError(err.message);
1641
+ throw err;
1642
+ } finally {
1643
+ setLoading(false);
1644
+ }
1645
+ }, []);
1646
+ return {
1647
+ send,
1648
+ schedule,
1649
+ loading,
1650
+ error
1651
+ };
1652
+ }
1653
+
1654
+ const PushContext = /*#__PURE__*/React.createContext(null);
1655
+ function PushProvider({
1656
+ config = {},
1657
+ children
1658
+ }) {
1659
+ const push = usePush(config);
1660
+ return /*#__PURE__*/React.createElement(PushContext.Provider, {
1661
+ value: push
1662
+ }, children);
1663
+ }
1664
+
1665
+ function PushPermissionBanner({
1666
+ onAllow,
1667
+ onDismiss,
1668
+ title = 'Enable Notifications',
1669
+ message = 'Stay updated with real-time push notifications',
1670
+ className = ''
1671
+ }) {
1672
+ return /*#__PURE__*/React.createElement("div", {
1673
+ className: `qps-banner ${className}`,
1674
+ style: {
1675
+ display: 'flex',
1676
+ alignItems: 'center',
1677
+ gap: 12,
1678
+ padding: '12px 16px',
1679
+ background: 'linear-gradient(135deg,#eff6ff,#eef2ff)',
1680
+ borderRadius: 12,
1681
+ border: '1px solid #bfdbfe'
1682
+ }
1683
+ }, /*#__PURE__*/React.createElement("svg", {
1684
+ width: "24",
1685
+ height: "24",
1686
+ viewBox: "0 0 24 24",
1687
+ fill: "none",
1688
+ stroke: "#3b82f6",
1689
+ strokeWidth: "2"
1690
+ }, /*#__PURE__*/React.createElement("path", {
1691
+ d: "M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"
1692
+ }), /*#__PURE__*/React.createElement("path", {
1693
+ d: "M13.73 21a2 2 0 01-3.46 0"
1694
+ })), /*#__PURE__*/React.createElement("div", {
1695
+ style: {
1696
+ flex: 1
1697
+ }
1698
+ }, /*#__PURE__*/React.createElement("div", {
1699
+ style: {
1700
+ fontSize: 14,
1701
+ fontWeight: 600,
1702
+ color: '#1e40af'
1703
+ }
1704
+ }, title), /*#__PURE__*/React.createElement("div", {
1705
+ style: {
1706
+ fontSize: 12,
1707
+ color: '#3b82f6',
1708
+ marginTop: 2
1709
+ }
1710
+ }, message)), /*#__PURE__*/React.createElement("button", {
1711
+ onClick: onAllow,
1712
+ style: {
1713
+ padding: '6px 16px',
1714
+ borderRadius: 8,
1715
+ border: 'none',
1716
+ background: '#3b82f6',
1717
+ color: '#fff',
1718
+ cursor: 'pointer',
1719
+ fontSize: 13,
1720
+ fontWeight: 600
1721
+ }
1722
+ }, "Allow"), onDismiss && /*#__PURE__*/React.createElement("button", {
1723
+ onClick: onDismiss,
1724
+ style: {
1725
+ border: 'none',
1726
+ background: 'none',
1727
+ cursor: 'pointer',
1728
+ color: '#93c5fd',
1729
+ fontSize: 16
1730
+ }
1731
+ }, "\u2715"));
1732
+ }
1733
+
1734
+ /**
1735
+ * Push SDK - React 组件
1736
+ * 消息推送可视化组件
1737
+ */
1738
+
1739
+
1740
+ /**
1741
+ * 通知中心
1742
+ */
1743
+ function NotificationCenter({
1744
+ onClose
1745
+ }) {
1746
+ const [filter, setFilter] = React.useState('all');
1747
+ const {
1748
+ notifications,
1749
+ unreadCount,
1750
+ loading,
1751
+ markAsRead,
1752
+ markAllAsRead,
1753
+ deleteNotification,
1754
+ clearAll
1755
+ } = useNotifications({
1756
+ filter
1757
+ });
1758
+ const filteredNotifications = filter === 'unread' ? notifications.filter(n => !n.read) : notifications;
1759
+ return /*#__PURE__*/React.createElement("div", {
1760
+ className: "eco-push-center"
1761
+ }, /*#__PURE__*/React.createElement("div", {
1762
+ className: "eco-push-header"
1763
+ }, /*#__PURE__*/React.createElement("h3", null, t('notifications')), unreadCount > 0 && /*#__PURE__*/React.createElement("span", {
1764
+ className: "eco-push-badge"
1765
+ }, unreadCount), onClose && /*#__PURE__*/React.createElement("button", {
1766
+ className: "eco-push-close",
1767
+ onClick: onClose
1768
+ }, "\xD7")), /*#__PURE__*/React.createElement("div", {
1769
+ className: "eco-push-tabs"
1770
+ }, /*#__PURE__*/React.createElement("button", {
1771
+ className: `eco-push-tab ${filter === 'all' ? 'active' : ''}`,
1772
+ onClick: () => setFilter('all')
1773
+ }, t('all')), /*#__PURE__*/React.createElement("button", {
1774
+ className: `eco-push-tab ${filter === 'unread' ? 'active' : ''}`,
1775
+ onClick: () => setFilter('unread')
1776
+ }, t('unread'), " ", unreadCount > 0 && `(${unreadCount})`)), /*#__PURE__*/React.createElement("div", {
1777
+ className: "eco-push-actions"
1778
+ }, /*#__PURE__*/React.createElement("button", {
1779
+ className: "eco-push-action-btn",
1780
+ onClick: markAllAsRead
1781
+ }, t('markAllRead')), /*#__PURE__*/React.createElement("button", {
1782
+ className: "eco-push-action-btn",
1783
+ onClick: clearAll
1784
+ }, t('clear'))), /*#__PURE__*/React.createElement("div", {
1785
+ className: "eco-push-list"
1786
+ }, loading ? /*#__PURE__*/React.createElement("div", {
1787
+ className: "eco-push-loading"
1788
+ }, /*#__PURE__*/React.createElement("div", {
1789
+ className: "eco-push-spinner"
1790
+ })) : filteredNotifications.length === 0 ? /*#__PURE__*/React.createElement("div", {
1791
+ className: "eco-push-empty"
1792
+ }, /*#__PURE__*/React.createElement("span", {
1793
+ className: "eco-push-empty-icon"
1794
+ }, "\uD83D\uDD14"), /*#__PURE__*/React.createElement("span", null, t('noNotifications'))) : filteredNotifications.map(notification => /*#__PURE__*/React.createElement(NotificationItem, {
1795
+ key: notification.id,
1796
+ notification: notification,
1797
+ onRead: () => markAsRead(notification.id),
1798
+ onDelete: () => deleteNotification(notification.id)
1799
+ }))));
1800
+ }
1801
+
1802
+ /**
1803
+ * 单个通知项
1804
+ */
1805
+ function NotificationItem({
1806
+ notification,
1807
+ onRead,
1808
+ onDelete
1809
+ }) {
1810
+ const handleClick = () => {
1811
+ if (!notification.read) {
1812
+ onRead?.();
1813
+ }
1814
+ if (notification.link) {
1815
+ window.location.href = notification.link;
1816
+ }
1817
+ };
1818
+ return /*#__PURE__*/React.createElement("div", {
1819
+ className: `eco-push-item ${notification.read ? '' : 'unread'}`,
1820
+ onClick: handleClick
1821
+ }, /*#__PURE__*/React.createElement("div", {
1822
+ className: "eco-push-item-icon"
1823
+ }, getTypeIcon(notification.type)), /*#__PURE__*/React.createElement("div", {
1824
+ className: "eco-push-item-content"
1825
+ }, /*#__PURE__*/React.createElement("div", {
1826
+ className: "eco-push-item-header"
1827
+ }, /*#__PURE__*/React.createElement("span", {
1828
+ className: "eco-push-item-title"
1829
+ }, notification.title), /*#__PURE__*/React.createElement("span", {
1830
+ className: "eco-push-item-time"
1831
+ }, formatTime(notification.createdAt))), /*#__PURE__*/React.createElement("p", {
1832
+ className: "eco-push-item-body"
1833
+ }, notification.body), notification.image && /*#__PURE__*/React.createElement("img", {
1834
+ className: "eco-push-item-image",
1835
+ src: notification.image,
1836
+ alt: ""
1837
+ })), /*#__PURE__*/React.createElement("button", {
1838
+ className: "eco-push-item-delete",
1839
+ onClick: e => {
1840
+ e.stopPropagation();
1841
+ onDelete?.();
1842
+ }
1843
+ }, "\xD7"));
1844
+ }
1845
+
1846
+ /**
1847
+ * 通知铃铛按钮
1848
+ */
1849
+ function NotificationBell({
1850
+ onClick
1851
+ }) {
1852
+ const {
1853
+ unreadCount
1854
+ } = useNotifications({
1855
+ limit: 0
1856
+ });
1857
+ return /*#__PURE__*/React.createElement("button", {
1858
+ className: "eco-push-bell",
1859
+ onClick: onClick
1860
+ }, /*#__PURE__*/React.createElement("span", {
1861
+ className: "eco-push-bell-icon"
1862
+ }, "\uD83D\uDD14"), unreadCount > 0 && /*#__PURE__*/React.createElement("span", {
1863
+ className: "eco-push-bell-badge"
1864
+ }, unreadCount > 99 ? '99+' : unreadCount));
1865
+ }
1866
+
1867
+ /**
1868
+ * 推送权限请求
1869
+ */
1870
+ function PushPermissionPrompt({
1871
+ onGranted,
1872
+ onDenied
1873
+ }) {
1874
+ const {
1875
+ permission,
1876
+ supported,
1877
+ requestPermission
1878
+ } = usePushPermission();
1879
+ const [requesting, setRequesting] = React.useState(false);
1880
+ if (!supported) return null;
1881
+ if (permission === 'granted') return null;
1882
+ if (permission === 'denied') {
1883
+ return /*#__PURE__*/React.createElement("div", {
1884
+ className: "eco-push-permission eco-push-permission-denied"
1885
+ }, /*#__PURE__*/React.createElement("span", {
1886
+ className: "eco-push-permission-icon"
1887
+ }, "\uD83D\uDD15"), /*#__PURE__*/React.createElement("p", null, t('permissionRequired')));
1888
+ }
1889
+ const handleRequest = async () => {
1890
+ setRequesting(true);
1891
+ const result = await requestPermission();
1892
+ setRequesting(false);
1893
+ if (result === 'granted') {
1894
+ onGranted?.();
1895
+ } else {
1896
+ onDenied?.();
1897
+ }
1898
+ };
1899
+ return /*#__PURE__*/React.createElement("div", {
1900
+ className: "eco-push-permission"
1901
+ }, /*#__PURE__*/React.createElement("span", {
1902
+ className: "eco-push-permission-icon"
1903
+ }, "\uD83D\uDD14"), /*#__PURE__*/React.createElement("div", {
1904
+ className: "eco-push-permission-content"
1905
+ }, /*#__PURE__*/React.createElement("h4", null, t('requestPermission')), /*#__PURE__*/React.createElement("p", null, "\u63A5\u6536\u91CD\u8981\u7684\u901A\u77E5\u548C\u66F4\u65B0")), /*#__PURE__*/React.createElement("button", {
1906
+ className: "eco-push-permission-btn",
1907
+ onClick: handleRequest,
1908
+ disabled: requesting
1909
+ }, requesting ? '...' : t('enablePush')));
1910
+ }
1911
+
1912
+ /**
1913
+ * 通知设置面板
1914
+ */
1915
+ function NotificationSettings() {
1916
+ const {
1917
+ preferences,
1918
+ loading,
1919
+ saving,
1920
+ toggleChannel,
1921
+ toggleType,
1922
+ updatePreferences
1923
+ } = useNotificationPreferences();
1924
+ if (loading) {
1925
+ return /*#__PURE__*/React.createElement("div", {
1926
+ className: "eco-push-settings eco-push-loading"
1927
+ }, /*#__PURE__*/React.createElement("div", {
1928
+ className: "eco-push-spinner"
1929
+ }));
1930
+ }
1931
+ return /*#__PURE__*/React.createElement("div", {
1932
+ className: "eco-push-settings"
1933
+ }, /*#__PURE__*/React.createElement("h3", null, t('preferences')), /*#__PURE__*/React.createElement("div", {
1934
+ className: "eco-push-settings-section"
1935
+ }, /*#__PURE__*/React.createElement("h4", null, t('channel')), /*#__PURE__*/React.createElement("div", {
1936
+ className: "eco-push-settings-list"
1937
+ }, Object.entries(preferences.channels || {}).map(([channel, enabled]) => /*#__PURE__*/React.createElement(ToggleItem, {
1938
+ key: channel,
1939
+ label: t(channel),
1940
+ checked: enabled,
1941
+ onChange: () => toggleChannel(channel),
1942
+ disabled: saving
1943
+ })))), /*#__PURE__*/React.createElement("div", {
1944
+ className: "eco-push-settings-section"
1945
+ }, /*#__PURE__*/React.createElement("h4", null, t('type')), /*#__PURE__*/React.createElement("div", {
1946
+ className: "eco-push-settings-list"
1947
+ }, Object.entries(preferences.types || {}).map(([type, enabled]) => /*#__PURE__*/React.createElement(ToggleItem, {
1948
+ key: type,
1949
+ label: t(type),
1950
+ checked: enabled,
1951
+ onChange: () => toggleType(type),
1952
+ disabled: saving
1953
+ })))), /*#__PURE__*/React.createElement("div", {
1954
+ className: "eco-push-settings-section"
1955
+ }, /*#__PURE__*/React.createElement("h4", null, t('quietHours')), /*#__PURE__*/React.createElement(ToggleItem, {
1956
+ label: t('enabled'),
1957
+ checked: preferences.quietHours?.enabled,
1958
+ onChange: () => updatePreferences({
1959
+ quietHours: {
1960
+ ...preferences.quietHours,
1961
+ enabled: !preferences.quietHours?.enabled
1962
+ }
1963
+ }),
1964
+ disabled: saving
1965
+ }), preferences.quietHours?.enabled && /*#__PURE__*/React.createElement("div", {
1966
+ className: "eco-push-quiet-hours"
1967
+ }, /*#__PURE__*/React.createElement("input", {
1968
+ type: "time",
1969
+ value: preferences.quietHours?.start || '22:00',
1970
+ onChange: e => updatePreferences({
1971
+ quietHours: {
1972
+ ...preferences.quietHours,
1973
+ start: e.target.value
1974
+ }
1975
+ })
1976
+ }), /*#__PURE__*/React.createElement("span", null, "-"), /*#__PURE__*/React.createElement("input", {
1977
+ type: "time",
1978
+ value: preferences.quietHours?.end || '08:00',
1979
+ onChange: e => updatePreferences({
1980
+ quietHours: {
1981
+ ...preferences.quietHours,
1982
+ end: e.target.value
1983
+ }
1984
+ })
1985
+ }))));
1986
+ }
1987
+
1988
+ /**
1989
+ * 开关项
1990
+ */
1991
+ function ToggleItem({
1992
+ label,
1993
+ checked,
1994
+ onChange,
1995
+ disabled
1996
+ }) {
1997
+ return /*#__PURE__*/React.createElement("div", {
1998
+ className: "eco-push-toggle-item"
1999
+ }, /*#__PURE__*/React.createElement("span", null, label), /*#__PURE__*/React.createElement("button", {
2000
+ className: `eco-push-toggle ${checked ? 'active' : ''}`,
2001
+ onClick: onChange,
2002
+ disabled: disabled
2003
+ }, /*#__PURE__*/React.createElement("span", {
2004
+ className: "eco-push-toggle-handle"
2005
+ })));
2006
+ }
2007
+
2008
+ /**
2009
+ * Toast通知
2010
+ */
2011
+ function NotificationToast({
2012
+ notification,
2013
+ onClose,
2014
+ duration = 5000
2015
+ }) {
2016
+ React.useEffect(() => {
2017
+ if (duration > 0) {
2018
+ const timer = setTimeout(onClose, duration);
2019
+ return () => clearTimeout(timer);
2020
+ }
2021
+ }, [duration, onClose]);
2022
+ return /*#__PURE__*/React.createElement("div", {
2023
+ className: "eco-push-toast"
2024
+ }, /*#__PURE__*/React.createElement("div", {
2025
+ className: "eco-push-toast-icon"
2026
+ }, getTypeIcon(notification.type)), /*#__PURE__*/React.createElement("div", {
2027
+ className: "eco-push-toast-content"
2028
+ }, /*#__PURE__*/React.createElement("div", {
2029
+ className: "eco-push-toast-title"
2030
+ }, notification.title), /*#__PURE__*/React.createElement("div", {
2031
+ className: "eco-push-toast-body"
2032
+ }, notification.body)), /*#__PURE__*/React.createElement("button", {
2033
+ className: "eco-push-toast-close",
2034
+ onClick: onClose
2035
+ }, "\xD7"));
2036
+ }
2037
+
2038
+ // 工具函数
2039
+ function getTypeIcon(type) {
2040
+ const icons = {
2041
+ system: '⚙️',
2042
+ marketing: '📢',
2043
+ transactional: '💳',
2044
+ social: '👥',
2045
+ reminder: '⏰',
2046
+ alert: '⚠️'
2047
+ };
2048
+ return icons[type] || '🔔';
2049
+ }
2050
+ function formatTime(timestamp) {
2051
+ const now = Date.now();
2052
+ const diff = now - new Date(timestamp).getTime();
2053
+ const minutes = Math.floor(diff / 60000);
2054
+ const hours = Math.floor(diff / 3600000);
2055
+ const days = Math.floor(diff / 86400000);
2056
+ if (minutes < 1) return t('now');
2057
+ if (minutes < 60) return t('minutesAgo', {
2058
+ n: minutes
2059
+ });
2060
+ if (hours < 24) return t('hoursAgo', {
2061
+ n: hours
2062
+ });
2063
+ if (days === 1) return t('yesterday');
2064
+ if (days < 7) return t('daysAgo', {
2065
+ n: days
2066
+ });
2067
+ return new Date(timestamp).toLocaleDateString();
2068
+ }
2069
+
2070
+ /**
2071
+ * @quantabit/push-sdk — 通用推送通知适配层
2072
+ *
2073
+ * 统一 API 接入多种推送服务:
2074
+ * - 浏览器原生 Web Push (Notification API + Service Worker)
2075
+ * - Firebase Cloud Messaging (FCM)
2076
+ * - OneSignal
2077
+ * - 极光推送 JPush
2078
+ * - WebSocket 自研推送
2079
+ * - Server-Sent Events (SSE)
2080
+ * - 自定义适配器
2081
+ *
2082
+ * 使用方式:
2083
+ * const push = new PushManager({ adapter: 'websocket', url: 'wss://...' });
2084
+ * push.subscribe();
2085
+ * push.onMessage(msg => console.log(msg));
2086
+ */
2087
+ const getNotifications = pushApi.getHistory.bind(pushApi);
2088
+ const markAsRead = pushApi.markAsRead.bind(pushApi);
2089
+ const markAllAsRead = async () => pushApi.batchMarkAsRead([]);
2090
+ const send = pushApi.send.bind(pushApi);
2091
+ const schedule = pushApi.schedule.bind(pushApi);
2092
+ const getPreferences = pushApi.getPreferences.bind(pushApi);
2093
+ const updatePreferences = pushApi.updatePreferences.bind(pushApi);
2094
+
2095
+ exports.FCMAdapter = FCMAdapter;
2096
+ exports.JPushAdapter = JPushAdapter;
2097
+ exports.MockAdapter = MockAdapter;
2098
+ exports.NotificationBell = NotificationBell;
2099
+ exports.NotificationCenter = NotificationCenter;
2100
+ exports.NotificationChannel = NotificationChannel;
2101
+ exports.NotificationItem = NotificationItem;
2102
+ exports.NotificationSettings = NotificationSettings;
2103
+ exports.NotificationStatus = NotificationStatus;
2104
+ exports.NotificationToast = NotificationToast;
2105
+ exports.NotificationType = NotificationType;
2106
+ exports.OneSignalAdapter = OneSignalAdapter;
2107
+ exports.PermissionStatus = PermissionStatus;
2108
+ exports.PushApiClient = PushApiClient;
2109
+ exports.PushChannel = PushChannel;
2110
+ exports.PushManager = PushManager;
2111
+ exports.PushPermissionBanner = PushPermissionBanner;
2112
+ exports.PushPermissionPrompt = PushPermissionPrompt;
2113
+ exports.PushPriority = PushPriority;
2114
+ exports.PushProvider = PushProvider;
2115
+ exports.PushStatus = PushStatus;
2116
+ exports.SSEAdapter = SSEAdapter;
2117
+ exports.SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGES;
2118
+ exports.SubscriptionStatus = SubscriptionStatus;
2119
+ exports.WebPushAdapter = WebPushAdapter;
2120
+ exports.WebPushClient = WebPushClient;
2121
+ exports.WebSocketAdapter = WebSocketAdapter;
2122
+ exports.getLanguage = getLanguage;
2123
+ exports.getNotifications = getNotifications;
2124
+ exports.getPreferences = getPreferences;
2125
+ exports.markAllAsRead = markAllAsRead;
2126
+ exports.markAsRead = markAsRead;
2127
+ exports.messages = messages;
2128
+ exports.pushApi = pushApi;
2129
+ exports.schedule = schedule;
2130
+ exports.send = send;
2131
+ exports.setLanguage = setLanguage;
2132
+ exports.t = t;
2133
+ exports.updatePreferences = updatePreferences;
2134
+ exports.useNotificationPreferences = useNotificationPreferences;
2135
+ exports.useNotifications = useNotifications;
2136
+ exports.usePush = usePush;
2137
+ exports.usePushPermission = usePushPermission;
2138
+ exports.usePushSubscription = usePushSubscription;
2139
+ exports.useRealtimeNotifications = useRealtimeNotifications;
2140
+ exports.useSendNotification = useSendNotification;
2141
+ exports.webPushClient = webPushClient;
2142
+ //# sourceMappingURL=index.cjs.map