@tokamakdev/plugin-notifications 0.1.0-beta.50
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/README.md +206 -0
- package/android/AndroidManifest.xml +30 -0
- package/android/Content.kt +47 -0
- package/android/NotificationsPlugin.kt +287 -0
- package/android/Notifier.kt +91 -0
- package/android/Push.kt +87 -0
- package/android/Schedule.kt +77 -0
- package/apple/Info.plist +10 -0
- package/apple/NotificationContent.swift +151 -0
- package/apple/NotificationsPlugin.swift +381 -0
- package/package.json +29 -0
- package/service-worker/index.ts +142 -0
- package/src/index.ts +145 -0
- package/tokamak-plugin.json +39 -0
- package/web/index.ts +180 -0
- package/web/protocol.ts +43 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "notifications",
|
|
4
|
+
"platforms": {
|
|
5
|
+
"macos": {
|
|
6
|
+
"class": "TokamakNotificationsPlugin",
|
|
7
|
+
"sources": ["apple/NotificationContent.swift", "apple/NotificationsPlugin.swift"],
|
|
8
|
+
"frameworks": ["UserNotifications"]
|
|
9
|
+
},
|
|
10
|
+
"ios": {
|
|
11
|
+
"class": "TokamakNotificationsPlugin",
|
|
12
|
+
"sources": ["apple/NotificationContent.swift", "apple/NotificationsPlugin.swift"],
|
|
13
|
+
"frameworks": ["UserNotifications"],
|
|
14
|
+
"plist": "apple/Info.plist"
|
|
15
|
+
},
|
|
16
|
+
"ios-simulator": {
|
|
17
|
+
"class": "TokamakNotificationsPlugin",
|
|
18
|
+
"sources": ["apple/NotificationContent.swift", "apple/NotificationsPlugin.swift"],
|
|
19
|
+
"frameworks": ["UserNotifications"],
|
|
20
|
+
"plist": "apple/Info.plist"
|
|
21
|
+
},
|
|
22
|
+
"android": {
|
|
23
|
+
"class": "com.tokamak.plugins.notifications.TokamakNotificationsPlugin",
|
|
24
|
+
"sources": [
|
|
25
|
+
"android/NotificationsPlugin.kt",
|
|
26
|
+
"android/Content.kt",
|
|
27
|
+
"android/Notifier.kt",
|
|
28
|
+
"android/Schedule.kt",
|
|
29
|
+
"android/Push.kt"
|
|
30
|
+
],
|
|
31
|
+
"manifest": "android/AndroidManifest.xml",
|
|
32
|
+
"dependencies": ["com.google.firebase:firebase-messaging:25.1.3"],
|
|
33
|
+
"permissions": [
|
|
34
|
+
"android.permission.POST_NOTIFICATIONS",
|
|
35
|
+
"android.permission.RECEIVE_BOOT_COMPLETED"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/web/index.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Listener,
|
|
3
|
+
Message,
|
|
4
|
+
NotificationContent,
|
|
5
|
+
OpenedNotification,
|
|
6
|
+
Permission,
|
|
7
|
+
SubscribeOptions,
|
|
8
|
+
Subscription,
|
|
9
|
+
} from "../src/index.js";
|
|
10
|
+
import {
|
|
11
|
+
MESSAGE,
|
|
12
|
+
OPENED,
|
|
13
|
+
OPENED_CACHE,
|
|
14
|
+
SUBSCRIPTION,
|
|
15
|
+
type ServiceWorkerMessage,
|
|
16
|
+
notificationData,
|
|
17
|
+
pluginData,
|
|
18
|
+
webPushSubscription,
|
|
19
|
+
} from "./protocol.js";
|
|
20
|
+
|
|
21
|
+
export function permission(): Promise<Permission> {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
resolve(fromBrowserPermission(notificationApi().permission));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function requestPermission(): Promise<Permission> {
|
|
28
|
+
return fromBrowserPermission(await notificationApi().requestPermission());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function show(notification: NotificationContent): Promise<void> {
|
|
32
|
+
const api = notificationApi();
|
|
33
|
+
if (api.permission !== "granted") throw notAllowed();
|
|
34
|
+
const options = {
|
|
35
|
+
body: notification.body,
|
|
36
|
+
tag: notification.id,
|
|
37
|
+
data: notificationData("local", notification.data),
|
|
38
|
+
};
|
|
39
|
+
const registration = await serviceWorkerRegistration();
|
|
40
|
+
if (registration) {
|
|
41
|
+
await registration.showNotification(notification.title, options);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
new api(notification.title, options);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new DOMException("Showing notifications needs a service worker here", "NotSupportedError");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function schedule(): Promise<void> {
|
|
52
|
+
return Promise.reject(
|
|
53
|
+
new DOMException("Scheduled notifications are not supported on the web", "NotSupportedError"),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getDelivered(): Promise<NotificationContent[]> {
|
|
58
|
+
const registration = await serviceWorkerRegistration();
|
|
59
|
+
const shown = (await registration?.getNotifications()) ?? [];
|
|
60
|
+
return shown.flatMap((notification) => {
|
|
61
|
+
const data = pluginData(notification.data);
|
|
62
|
+
if (!data) return [];
|
|
63
|
+
return [{
|
|
64
|
+
id: notification.tag,
|
|
65
|
+
title: notification.title,
|
|
66
|
+
body: notification.body,
|
|
67
|
+
data: data.data,
|
|
68
|
+
}];
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function remove(id: string): Promise<void> {
|
|
73
|
+
const registration = await serviceWorkerRegistration();
|
|
74
|
+
for (const notification of (await registration?.getNotifications({ tag: id })) ?? []) {
|
|
75
|
+
notification.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function subscribe(options: SubscribeOptions): Promise<Subscription> {
|
|
80
|
+
if (notificationApi().permission !== "granted") throw notAllowed();
|
|
81
|
+
const registration = await serviceWorkerRegistration();
|
|
82
|
+
if (!registration) {
|
|
83
|
+
throw new DOMException("Web Push needs a registered service worker", "InvalidStateError");
|
|
84
|
+
}
|
|
85
|
+
const existing = await registration.pushManager.getSubscription();
|
|
86
|
+
if (existing) return webPushSubscription(existing);
|
|
87
|
+
if (!options.applicationServerKey) {
|
|
88
|
+
throw new DOMException("Web Push needs the applicationServerKey option", "InvalidStateError");
|
|
89
|
+
}
|
|
90
|
+
const subscription = await registration.pushManager.subscribe({
|
|
91
|
+
userVisibleOnly: true,
|
|
92
|
+
applicationServerKey: options.applicationServerKey,
|
|
93
|
+
});
|
|
94
|
+
return webPushSubscription(subscription);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function getSubscription(): Promise<Subscription | null> {
|
|
98
|
+
const registration = await serviceWorkerRegistration();
|
|
99
|
+
const subscription = await registration?.pushManager.getSubscription();
|
|
100
|
+
return subscription ? webPushSubscription(subscription) : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function unsubscribe(): Promise<void> {
|
|
104
|
+
const registration = await serviceWorkerRegistration();
|
|
105
|
+
await (await registration?.pushManager.getSubscription())?.unsubscribe();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function onMessage(listener: Listener<Message>): () => void {
|
|
109
|
+
return onServiceWorkerMessage((message) => {
|
|
110
|
+
if (message.type === MESSAGE) listener(message.message);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function onSubscriptionChange(listener: Listener<Subscription>): () => void {
|
|
115
|
+
return onServiceWorkerMessage((message) => {
|
|
116
|
+
if (message.type === SUBSCRIPTION) listener(message.subscription);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const openedListeners = new Set<Listener<OpenedNotification>>();
|
|
121
|
+
|
|
122
|
+
/** Delivers opened notifications the service worker holds, then each one it reports. */
|
|
123
|
+
export function onNotificationOpened(listener: Listener<OpenedNotification>): () => void {
|
|
124
|
+
openedListeners.add(listener);
|
|
125
|
+
const stop = onServiceWorkerMessage((message) => {
|
|
126
|
+
if (message.type === OPENED) void takeOpened();
|
|
127
|
+
});
|
|
128
|
+
void takeOpened();
|
|
129
|
+
return () => {
|
|
130
|
+
openedListeners.delete(listener);
|
|
131
|
+
stop();
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Takes each held notification once, even when several pages take them at once. */
|
|
136
|
+
async function takeOpened(): Promise<void> {
|
|
137
|
+
if (openedListeners.size === 0 || !("caches" in globalThis)) return;
|
|
138
|
+
const cache = await caches.open(OPENED_CACHE);
|
|
139
|
+
for (const request of await cache.keys()) {
|
|
140
|
+
const response = await cache.match(request);
|
|
141
|
+
if (!response || !(await cache.delete(request))) continue;
|
|
142
|
+
const opened = (await response.json()) as OpenedNotification;
|
|
143
|
+
for (const listener of openedListeners) listener(opened);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onServiceWorkerMessage(listener: Listener<ServiceWorkerMessage>): () => void {
|
|
148
|
+
const container = serviceWorkerContainer();
|
|
149
|
+
if (!container) return () => undefined;
|
|
150
|
+
const receive = (event: MessageEvent<ServiceWorkerMessage>) => {
|
|
151
|
+
listener(event.data);
|
|
152
|
+
};
|
|
153
|
+
container.addEventListener("message", receive);
|
|
154
|
+
return () => {
|
|
155
|
+
container.removeEventListener("message", receive);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function notificationApi(): typeof Notification {
|
|
160
|
+
const api = (globalThis as { Notification?: typeof Notification }).Notification;
|
|
161
|
+
if (api) return api;
|
|
162
|
+
throw new DOMException("Notifications are not supported here", "NotSupportedError");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function serviceWorkerContainer(): ServiceWorkerContainer | undefined {
|
|
166
|
+
return (globalThis as { navigator?: { serviceWorker?: ServiceWorkerContainer } }).navigator
|
|
167
|
+
?.serviceWorker;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function serviceWorkerRegistration(): Promise<ServiceWorkerRegistration | undefined> {
|
|
171
|
+
return serviceWorkerContainer()?.getRegistration();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function fromBrowserPermission(permission: NotificationPermission): Permission {
|
|
175
|
+
return permission === "default" ? "prompt" : permission;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function notAllowed(): DOMException {
|
|
179
|
+
return new DOMException("Notification permission has not been granted", "NotAllowedError");
|
|
180
|
+
}
|
package/web/protocol.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Message, OpenedNotification, Subscription } from "../src/index.js";
|
|
2
|
+
|
|
3
|
+
/** Messages the service worker helper posts to pages. */
|
|
4
|
+
export const MESSAGE = "tokamak-notifications:message";
|
|
5
|
+
export const OPENED = "tokamak-notifications:opened";
|
|
6
|
+
export const SUBSCRIPTION = "tokamak-notifications:subscription";
|
|
7
|
+
|
|
8
|
+
/** Holds opened notifications until a page takes them. */
|
|
9
|
+
export const OPENED_CACHE = "tokamak-notifications-opened";
|
|
10
|
+
|
|
11
|
+
/** What the plugin stores in `Notification.data`. */
|
|
12
|
+
export interface NotificationData {
|
|
13
|
+
readonly tokamak: {
|
|
14
|
+
readonly source: OpenedNotification["source"];
|
|
15
|
+
readonly data: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ServiceWorkerMessage =
|
|
20
|
+
| { readonly type: typeof MESSAGE; readonly message: Message }
|
|
21
|
+
| { readonly type: typeof OPENED }
|
|
22
|
+
| { readonly type: typeof SUBSCRIPTION; readonly subscription: Subscription };
|
|
23
|
+
|
|
24
|
+
export function notificationData(
|
|
25
|
+
source: OpenedNotification["source"],
|
|
26
|
+
data: Record<string, unknown> = {},
|
|
27
|
+
): NotificationData {
|
|
28
|
+
return { tokamak: { source, data } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The plugin's data on a notification it showed, or undefined for others. */
|
|
32
|
+
export function pluginData(value: unknown): NotificationData["tokamak"] | undefined {
|
|
33
|
+
return (value as Partial<NotificationData> | null)?.tokamak;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function webPushSubscription(subscription: PushSubscription): Subscription {
|
|
37
|
+
const { keys } = subscription.toJSON();
|
|
38
|
+
return {
|
|
39
|
+
service: "webpush",
|
|
40
|
+
endpoint: subscription.endpoint,
|
|
41
|
+
keys: { p256dh: keys?.p256dh ?? "", auth: keys?.auth ?? "" },
|
|
42
|
+
};
|
|
43
|
+
}
|