@voltro/plugin-notifications 0.51.0 → 0.53.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 +356 -0
- package/THIRD-PARTY-NOTICES.md +169 -1
- package/dist/index.d.ts +202 -4
- package/dist/index.js +626 -227
- package/dist/rpc.d.ts +27 -0
- package/dist/rpc.js +60 -2
- package/dist/web.d.ts +22 -0
- package/dist/web.js +81 -22
- package/package.json +8 -6
- package/sw.js +50 -0
package/dist/rpc.d.ts
CHANGED
|
@@ -79,4 +79,31 @@ export declare const unsubscribeDescriptor: MutationProcedureDescriptor<"notific
|
|
|
79
79
|
ok: typeof Schema.Boolean;
|
|
80
80
|
}>, typeof Schema.Never>;
|
|
81
81
|
|
|
82
|
+
export declare const webPushPublicKeyDescriptor: QueryProcedureDescriptor<"notifications.webPushPublicKey", Schema.Struct<{}>, Schema.Struct<{
|
|
83
|
+
key: typeof Schema.String;
|
|
84
|
+
}>, typeof Schema.Never>;
|
|
85
|
+
|
|
86
|
+
/** Registered only when the app configures a web-push channel — see
|
|
87
|
+
* `notificationsPlugin`. */
|
|
88
|
+
export declare const webPushRpcClientImports: ReadonlyArray<PluginRpcClientDescriptor>;
|
|
89
|
+
|
|
90
|
+
export declare const webPushStatusDescriptor: QueryProcedureDescriptor<"notifications.webPushStatus", Schema.Struct<{}>, Schema.Struct<{
|
|
91
|
+
endpoints: typeof Schema.Number;
|
|
92
|
+
}>, typeof Schema.Never>;
|
|
93
|
+
|
|
94
|
+
export declare const webPushSubscribeDescriptor: MutationProcedureDescriptor<"notifications.webPushSubscribe", Schema.Struct<{
|
|
95
|
+
endpoint: typeof Schema.String;
|
|
96
|
+
p256dh: typeof Schema.String;
|
|
97
|
+
auth: typeof Schema.String;
|
|
98
|
+
ua: Schema.optional<typeof Schema.String>;
|
|
99
|
+
}>, Schema.Struct<{
|
|
100
|
+
ok: typeof Schema.Boolean;
|
|
101
|
+
}>, typeof Schema.Never>;
|
|
102
|
+
|
|
103
|
+
export declare const webPushUnsubscribeDescriptor: MutationProcedureDescriptor<"notifications.webPushUnsubscribe", Schema.Struct<{
|
|
104
|
+
endpoint: typeof Schema.String;
|
|
105
|
+
}>, Schema.Struct<{
|
|
106
|
+
ok: typeof Schema.Boolean;
|
|
107
|
+
}>, typeof Schema.Never>;
|
|
108
|
+
|
|
82
109
|
export { }
|
package/dist/rpc.js
CHANGED
|
@@ -84,7 +84,65 @@ var r = n({
|
|
|
84
84
|
input: e.Struct({}),
|
|
85
85
|
output: e.Struct({ ok: e.Boolean }),
|
|
86
86
|
openAccess: "self-scoped write: clears only the calling subject's own quiet-hours window"
|
|
87
|
-
}), g =
|
|
87
|
+
}), g = n({
|
|
88
|
+
name: "notifications.webPushPublicKey",
|
|
89
|
+
input: e.Struct({}),
|
|
90
|
+
output: e.Struct({ key: e.String }),
|
|
91
|
+
openAccess: "public application-server key — the value every subscribing browser is handed by design"
|
|
92
|
+
}), _ = t({
|
|
93
|
+
name: "notifications.webPushSubscribe",
|
|
94
|
+
input: e.Struct({
|
|
95
|
+
endpoint: e.String,
|
|
96
|
+
p256dh: e.String,
|
|
97
|
+
auth: e.String,
|
|
98
|
+
ua: e.optional(e.String)
|
|
99
|
+
}),
|
|
100
|
+
output: e.Struct({ ok: e.Boolean }),
|
|
101
|
+
openAccess: "self-scoped write: registers the calling subject's own browser subscription (the subject is never caller-supplied)"
|
|
102
|
+
}), v = t({
|
|
103
|
+
name: "notifications.webPushUnsubscribe",
|
|
104
|
+
input: e.Struct({ endpoint: e.String }),
|
|
105
|
+
output: e.Struct({ ok: e.Boolean }),
|
|
106
|
+
openAccess: "self-scoped write: removes only a subscription the calling subject owns"
|
|
107
|
+
}), y = n({
|
|
108
|
+
name: "notifications.webPushStatus",
|
|
109
|
+
input: e.Struct({}),
|
|
110
|
+
output: e.Struct({ endpoints: e.Number }),
|
|
111
|
+
openAccess: "self-scoped read: counts only the calling subject's own subscriptions"
|
|
112
|
+
}), b = [
|
|
113
|
+
{
|
|
114
|
+
tag: "notifications.webPushPublicKey",
|
|
115
|
+
kind: "query",
|
|
116
|
+
import: {
|
|
117
|
+
module: "@voltro/plugin-notifications/rpc",
|
|
118
|
+
name: "webPushPublicKeyDescriptor"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
tag: "notifications.webPushSubscribe",
|
|
123
|
+
kind: "mutation",
|
|
124
|
+
import: {
|
|
125
|
+
module: "@voltro/plugin-notifications/rpc",
|
|
126
|
+
name: "webPushSubscribeDescriptor"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
tag: "notifications.webPushUnsubscribe",
|
|
131
|
+
kind: "mutation",
|
|
132
|
+
import: {
|
|
133
|
+
module: "@voltro/plugin-notifications/rpc",
|
|
134
|
+
name: "webPushUnsubscribeDescriptor"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
tag: "notifications.webPushStatus",
|
|
139
|
+
kind: "query",
|
|
140
|
+
import: {
|
|
141
|
+
module: "@voltro/plugin-notifications/rpc",
|
|
142
|
+
name: "webPushStatusDescriptor"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
], x = [
|
|
88
146
|
{
|
|
89
147
|
tag: "notifications.inbox",
|
|
90
148
|
kind: "query",
|
|
@@ -191,4 +249,4 @@ var r = n({
|
|
|
191
249
|
}
|
|
192
250
|
];
|
|
193
251
|
//#endregion
|
|
194
|
-
export { c as archiveDescriptor, h as clearQuietHoursDescriptor, r as inboxDescriptor, s as markAllReadDescriptor, a as markReadDescriptor, o as markUnreadDescriptor,
|
|
252
|
+
export { c as archiveDescriptor, h as clearQuietHoursDescriptor, r as inboxDescriptor, s as markAllReadDescriptor, a as markReadDescriptor, o as markUnreadDescriptor, x as notificationsRpcClientImports, u as preferencesDescriptor, d as setPreferenceDescriptor, m as setQuietHoursDescriptor, f as subscribeDescriptor, l as unarchiveDescriptor, i as unreadCountDescriptor, p as unsubscribeDescriptor, g as webPushPublicKeyDescriptor, b as webPushRpcClientImports, y as webPushStatusDescriptor, _ as webPushSubscribeDescriptor, v as webPushUnsubscribeDescriptor };
|
package/dist/web.d.ts
CHANGED
|
@@ -45,4 +45,26 @@ export declare const useTopicSubscription: (apiName?: string) => {
|
|
|
45
45
|
/** The caller's unread count. */
|
|
46
46
|
export declare const useUnreadCount: (apiName?: string) => number;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Permission flow + service-worker registration + subscribe/unsubscribe for
|
|
50
|
+
* the web-push channel. The service worker file is the app's to serve —
|
|
51
|
+
* copy `@voltro/plugin-notifications/sw.js` into `public/` (its scope decides
|
|
52
|
+
* which pages the worker controls; serving it from the root covers the app).
|
|
53
|
+
* Requires HTTPS (or localhost) — a push subscription is refused elsewhere.
|
|
54
|
+
*/
|
|
55
|
+
export declare const useWebPush: (opts?: {
|
|
56
|
+
swUrl?: string;
|
|
57
|
+
apiName?: string;
|
|
58
|
+
}) => WebPushState;
|
|
59
|
+
|
|
60
|
+
export declare interface WebPushState {
|
|
61
|
+
/** `unsupported` — no service worker / PushManager (or iOS Safari outside an
|
|
62
|
+
* installed PWA). `denied` — the user refused notification permission.
|
|
63
|
+
* `subscribed` reflects THIS browser's registration. */
|
|
64
|
+
readonly status: 'unsupported' | 'denied' | 'idle' | 'working' | 'subscribed' | 'error';
|
|
65
|
+
readonly error?: string;
|
|
66
|
+
readonly subscribe: () => Promise<void>;
|
|
67
|
+
readonly unsubscribe: () => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
export { }
|
package/dist/web.js
CHANGED
|
@@ -1,33 +1,92 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect as e, useState as t } from "react";
|
|
2
|
+
import { useMutation as n, useSubscription as r } from "@voltro/client";
|
|
2
3
|
//#region src/web.ts
|
|
3
|
-
var
|
|
4
|
-
let { data:
|
|
5
|
-
return
|
|
6
|
-
},
|
|
7
|
-
let { data:
|
|
8
|
-
return
|
|
9
|
-
},
|
|
10
|
-
let
|
|
11
|
-
return (e) =>
|
|
12
|
-
},
|
|
13
|
-
let
|
|
14
|
-
return (e,
|
|
4
|
+
var i = (e = {}, t = "app") => {
|
|
5
|
+
let { data: n } = r(t, "notifications.inbox", e);
|
|
6
|
+
return n ?? [];
|
|
7
|
+
}, a = (e = "app") => {
|
|
8
|
+
let { data: t } = r(e, "notifications.unreadCount", {});
|
|
9
|
+
return t?.count ?? 0;
|
|
10
|
+
}, o = (e = "app") => {
|
|
11
|
+
let t = n(e, "notifications.markRead");
|
|
12
|
+
return (e) => t.mutate({ id: e });
|
|
13
|
+
}, s = (e = "app") => {
|
|
14
|
+
let t = n(e, "notifications.setPreference");
|
|
15
|
+
return (e, n, r) => t.mutate({
|
|
15
16
|
category: e,
|
|
16
|
-
channel:
|
|
17
|
+
channel: n,
|
|
17
18
|
enabled: r
|
|
18
19
|
});
|
|
19
|
-
},
|
|
20
|
-
let
|
|
20
|
+
}, c = (e = "app") => {
|
|
21
|
+
let t = n(e, "notifications.subscribe"), r = n(e, "notifications.unsubscribe");
|
|
21
22
|
return {
|
|
22
|
-
subscribe: (e) =>
|
|
23
|
+
subscribe: (e) => t.mutate({ topic: e }),
|
|
23
24
|
unsubscribe: (e) => r.mutate({ topic: e })
|
|
24
25
|
};
|
|
25
|
-
},
|
|
26
|
-
let
|
|
26
|
+
}, l = (e) => {
|
|
27
|
+
let t = e.replace(/-/g, "+").replace(/_/g, "/"), n = atob(t), r = new Uint8Array(n.length);
|
|
28
|
+
for (let e = 0; e < n.length; e++) r[e] = n.charCodeAt(e);
|
|
29
|
+
return r;
|
|
30
|
+
}, u = (i = {}) => {
|
|
31
|
+
let a = i.apiName ?? "app", o = i.swUrl ?? "/sw.js", { data: s } = r(a, "notifications.webPushPublicKey", {}), c = n(a, "notifications.webPushSubscribe"), u = n(a, "notifications.webPushUnsubscribe"), d = typeof navigator < "u" && "serviceWorker" in navigator && typeof window < "u" && "PushManager" in window, [f, p] = t(d ? "idle" : "unsupported"), [m, h] = t(void 0);
|
|
32
|
+
e(() => {
|
|
33
|
+
if (!d) return;
|
|
34
|
+
let e = !1;
|
|
35
|
+
return navigator.serviceWorker.getRegistration(o).then(async (t) => {
|
|
36
|
+
let n = await t?.pushManager.getSubscription();
|
|
37
|
+
!e && n && p("subscribed");
|
|
38
|
+
}).catch(() => {}), () => {
|
|
39
|
+
e = !0;
|
|
40
|
+
};
|
|
41
|
+
}, [d, o]);
|
|
42
|
+
let g = async () => {
|
|
43
|
+
if (d) {
|
|
44
|
+
p("working"), h(void 0);
|
|
45
|
+
try {
|
|
46
|
+
if (await Notification.requestPermission() !== "granted") {
|
|
47
|
+
p("denied");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (s?.key === void 0) throw Error("application server key not loaded yet — retry in a moment");
|
|
51
|
+
let e = await navigator.serviceWorker.register(o);
|
|
52
|
+
await navigator.serviceWorker.ready;
|
|
53
|
+
let t = await e.pushManager.subscribe({
|
|
54
|
+
userVisibleOnly: !0,
|
|
55
|
+
applicationServerKey: l(s.key)
|
|
56
|
+
}), n = t.toJSON();
|
|
57
|
+
await c.mutate({
|
|
58
|
+
endpoint: t.endpoint,
|
|
59
|
+
p256dh: n.keys?.p256dh ?? "",
|
|
60
|
+
auth: n.keys?.auth ?? "",
|
|
61
|
+
ua: navigator.userAgent
|
|
62
|
+
}), p("subscribed");
|
|
63
|
+
} catch (e) {
|
|
64
|
+
h(e instanceof Error ? e.message : String(e)), p("error");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, _ = async () => {
|
|
68
|
+
if (d) {
|
|
69
|
+
p("working"), h(void 0);
|
|
70
|
+
try {
|
|
71
|
+
let e = await (await navigator.serviceWorker.getRegistration(o))?.pushManager.getSubscription();
|
|
72
|
+
e && (await u.mutate({ endpoint: e.endpoint }), await e.unsubscribe()), p("idle");
|
|
73
|
+
} catch (e) {
|
|
74
|
+
h(e instanceof Error ? e.message : String(e)), p("error");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
status: f,
|
|
80
|
+
...m === void 0 ? {} : { error: m },
|
|
81
|
+
subscribe: g,
|
|
82
|
+
unsubscribe: _
|
|
83
|
+
};
|
|
84
|
+
}, d = (e = "app") => {
|
|
85
|
+
let t = n(e, "notifications.setQuietHours"), r = n(e, "notifications.clearQuietHours");
|
|
27
86
|
return {
|
|
28
|
-
set: (e,
|
|
87
|
+
set: (e, n, r) => t.mutate({
|
|
29
88
|
startMinute: e,
|
|
30
|
-
endMinute:
|
|
89
|
+
endMinute: n,
|
|
31
90
|
...r?.tz ? { tz: r.tz } : {},
|
|
32
91
|
...r?.policy ? { policy: r.policy } : {}
|
|
33
92
|
}),
|
|
@@ -35,4 +94,4 @@ var n = (e = {}, n = "app") => {
|
|
|
35
94
|
};
|
|
36
95
|
};
|
|
37
96
|
//#endregion
|
|
38
|
-
export {
|
|
97
|
+
export { i as useInbox, o as useMarkRead, d as useQuietHours, s as useSetNotificationPreference, c as useTopicSubscription, a as useUnreadCount, u as useWebPush };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-notifications",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "One unified notification surface — email, Slack/webhook, SMS, push, in-app — with per-subject preference routing, an in-app inbox, and a delivery log. NotificationService + typed routes + useInbox/useUnreadCount hooks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"import": "./dist/rpc.js",
|
|
34
34
|
"default": "./dist/rpc.js"
|
|
35
35
|
},
|
|
36
|
-
"./package.json": "./package.json"
|
|
36
|
+
"./package.json": "./package.json",
|
|
37
|
+
"./sw.js": "./sw.js"
|
|
37
38
|
},
|
|
38
39
|
"main": "./dist/index.js",
|
|
39
40
|
"module": "./dist/index.js",
|
|
@@ -43,12 +44,13 @@
|
|
|
43
44
|
"node": ">=24.0.0"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@voltro/client": "0.
|
|
47
|
-
"@voltro/database": "0.
|
|
48
|
-
"@voltro/protocol": "0.
|
|
47
|
+
"@voltro/client": "0.53.0",
|
|
48
|
+
"@voltro/database": "0.53.0",
|
|
49
|
+
"@voltro/protocol": "0.53.0"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
|
-
"effect": "^3.22.0"
|
|
52
|
+
"effect": "^3.22.0",
|
|
53
|
+
"react": "^19.0.0"
|
|
52
54
|
},
|
|
53
55
|
"publishConfig": {
|
|
54
56
|
"access": "public"
|
package/sw.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @voltro/plugin-notifications — the web-push service worker.
|
|
2
|
+
//
|
|
3
|
+
// Copy this file into your web app's `public/` directory (conventionally as
|
|
4
|
+
// `/sw.js` — its URL decides its scope, and the root covers the whole app).
|
|
5
|
+
// It shows the pushed notification, opens the declared `url` on click, and
|
|
6
|
+
// reports the click back to the delivery log via the click token.
|
|
7
|
+
//
|
|
8
|
+
// Self-contained on purpose: a service worker loads outside the bundler, so
|
|
9
|
+
// nothing here may import anything.
|
|
10
|
+
|
|
11
|
+
self.addEventListener('push', (event) => {
|
|
12
|
+
let payload = {}
|
|
13
|
+
try { payload = event.data ? event.data.json() : {} } catch { /* non-JSON push stays an empty notification */ }
|
|
14
|
+
const title = typeof payload.title === 'string' ? payload.title : 'Notification'
|
|
15
|
+
event.waitUntil((async () => {
|
|
16
|
+
// Tell open pages FIRST — an in-page inbox can refresh without a reload,
|
|
17
|
+
// and the page signal must not depend on the notification display (which
|
|
18
|
+
// a headless or focus-suppressed environment may decline).
|
|
19
|
+
const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
20
|
+
for (const client of clientList) client.postMessage({ type: 'voltro:push', payload })
|
|
21
|
+
await self.registration.showNotification(title, {
|
|
22
|
+
body: typeof payload.body === 'string' ? payload.body : '',
|
|
23
|
+
data: { url: payload.url, clickToken: payload.clickToken },
|
|
24
|
+
...(payload.data && typeof payload.data.badge === 'number' ? { badge: payload.data.badge } : {}),
|
|
25
|
+
})
|
|
26
|
+
})())
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
self.addEventListener('notificationclick', (event) => {
|
|
30
|
+
event.notification.close()
|
|
31
|
+
const data = event.notification.data || {}
|
|
32
|
+
event.waitUntil((async () => {
|
|
33
|
+
// Best-effort click report — the token is the capability, same-origin only.
|
|
34
|
+
if (typeof data.clickToken === 'string' && data.clickToken !== '') {
|
|
35
|
+
try {
|
|
36
|
+
await fetch('/_voltro/notifications/clicked', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'content-type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ token: data.clickToken }),
|
|
40
|
+
})
|
|
41
|
+
} catch { /* a failed report never blocks the open */ }
|
|
42
|
+
}
|
|
43
|
+
const url = typeof data.url === 'string' && data.url !== '' ? data.url : '/'
|
|
44
|
+
const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
45
|
+
for (const client of clientList) {
|
|
46
|
+
if ('focus' in client) { await client.focus(); if ('navigate' in client) await client.navigate(url); return }
|
|
47
|
+
}
|
|
48
|
+
await self.clients.openWindow(url)
|
|
49
|
+
})())
|
|
50
|
+
})
|