@voltro/plugin-notifications 0.52.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/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, g as notificationsRpcClientImports, u as preferencesDescriptor, d as setPreferenceDescriptor, m as setQuietHoursDescriptor, f as subscribeDescriptor, l as unarchiveDescriptor, i as unreadCountDescriptor, p as unsubscribeDescriptor };
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 { useMutation as e, useSubscription as t } from "@voltro/client";
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 n = (e = {}, n = "app") => {
4
- let { data: r } = t(n, "notifications.inbox", e);
5
- return r ?? [];
6
- }, r = (e = "app") => {
7
- let { data: n } = t(e, "notifications.unreadCount", {});
8
- return n?.count ?? 0;
9
- }, i = (t = "app") => {
10
- let n = e(t, "notifications.markRead");
11
- return (e) => n.mutate({ id: e });
12
- }, a = (t = "app") => {
13
- let n = e(t, "notifications.setPreference");
14
- return (e, t, r) => n.mutate({
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: t,
17
+ channel: n,
17
18
  enabled: r
18
19
  });
19
- }, o = (t = "app") => {
20
- let n = e(t, "notifications.subscribe"), r = e(t, "notifications.unsubscribe");
20
+ }, c = (e = "app") => {
21
+ let t = n(e, "notifications.subscribe"), r = n(e, "notifications.unsubscribe");
21
22
  return {
22
- subscribe: (e) => n.mutate({ topic: e }),
23
+ subscribe: (e) => t.mutate({ topic: e }),
23
24
  unsubscribe: (e) => r.mutate({ topic: e })
24
25
  };
25
- }, s = (t = "app") => {
26
- let n = e(t, "notifications.setQuietHours"), r = e(t, "notifications.clearQuietHours");
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, t, r) => n.mutate({
87
+ set: (e, n, r) => t.mutate({
29
88
  startMinute: e,
30
- endMinute: t,
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 { n as useInbox, i as useMarkRead, s as useQuietHours, a as useSetNotificationPreference, o as useTopicSubscription, r as useUnreadCount };
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.52.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.52.0",
47
- "@voltro/database": "0.52.0",
48
- "@voltro/protocol": "0.52.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
+ })