this repo has no description
1// Utils for push notifications
2import { api } from './api';
3import { getVapidKey } from './store-utils';
4
5// Subscription is an object with the following structure:
6// {
7// data: {
8// alerts: {
9// admin: {
10// report: boolean,
11// signUp: boolean,
12// },
13// favourite: boolean,
14// follow: boolean,
15// mention: boolean,
16// poll: boolean,
17// reblog: boolean,
18// status: boolean,
19// update: boolean,
20// }
21// },
22// policy: "all" | "followed" | "follower" | "none",
23// subscription: {
24// endpoint: string,
25// keys: {
26// auth: string,
27// p256dh: string,
28// },
29// },
30// }
31
32// Back-end CRUD
33// =============
34
35function createBackendPushSubscription(subscription) {
36 const { masto } = api();
37 return masto.v1.push.subscription.create(subscription);
38}
39
40function fetchBackendPushSubscription() {
41 const { masto } = api();
42 return masto.v1.push.subscription.fetch();
43}
44
45function updateBackendPushSubscription(subscription) {
46 const { masto } = api();
47 return masto.v1.push.subscription.update(subscription);
48}
49
50function removeBackendPushSubscription() {
51 const { masto } = api();
52 return masto.v1.push.subscription.remove();
53}
54
55// Front-end
56// =========
57
58export function isPushSupported() {
59 return 'serviceWorker' in navigator && 'PushManager' in window;
60}
61
62export function getRegistration() {
63 // return navigator.serviceWorker.ready;
64 return navigator.serviceWorker.getRegistration();
65}
66
67async function getSubscription() {
68 const registration = await getRegistration();
69 const subscription = registration
70 ? await registration.pushManager.getSubscription()
71 : undefined;
72 return { registration, subscription };
73}
74
75function urlBase64ToUint8Array(base64String) {
76 const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
77 const base64 = `${base64String}${padding}`
78 .replace(/-/g, '+')
79 .replace(/_/g, '/');
80
81 const rawData = window.atob(base64);
82 const outputArray = new Uint8Array(rawData.length);
83
84 for (let i = 0; i < rawData.length; ++i) {
85 outputArray[i] = rawData.charCodeAt(i);
86 }
87
88 return outputArray;
89}
90
91// Front-end <-> back-end
92// ======================
93
94export async function initSubscription() {
95 if (!isPushSupported()) return;
96 const { subscription } = await getSubscription();
97 let backendSubscription = null;
98 try {
99 backendSubscription = await fetchBackendPushSubscription();
100 } catch (err) {
101 if (/(not found|unknown)/i.test(err.message)) {
102 // No subscription found
103 } else {
104 // Other error
105 throw err;
106 }
107 }
108 console.log('INIT subscription', {
109 subscription,
110 backendSubscription,
111 });
112
113 // Check if the subscription changed
114 if (backendSubscription && subscription) {
115 const sameEndpoint = backendSubscription.endpoint === subscription.endpoint;
116 const vapidKey = getVapidKey();
117 const sameKey = backendSubscription.serverKey === vapidKey;
118 if (!sameEndpoint) {
119 throw new Error('Backend subscription endpoint changed');
120 }
121 if (sameKey) {
122 // Subscription didn't change
123 } else {
124 // Subscription changed
125 console.error('🔔 Subscription changed', {
126 sameEndpoint,
127 serverKey: backendSubscription.serverKey,
128 vapIdKey: vapidKey,
129 endpoint1: backendSubscription.endpoint,
130 endpoint2: subscription.endpoint,
131 sameKey,
132 key1: backendSubscription.serverKey,
133 key2: vapidKey,
134 });
135 throw new Error('Backend subscription key and vapid key changed');
136 // Only unsubscribe from backend, not from browser
137 // await removeBackendPushSubscription();
138 // // Now let's resubscribe
139 // // NOTE: I have no idea if this works
140 // return await updateSubscription({
141 // data: backendSubscription.data,
142 // policy: backendSubscription.policy,
143 // });
144 }
145 }
146
147 if (subscription && !backendSubscription) {
148 // check if account's vapidKey is same as subscription's applicationServerKey
149 const vapidKey = getVapidKey();
150 if (vapidKey) {
151 const { applicationServerKey } = subscription.options;
152 const vapidKeyStr = urlBase64ToUint8Array(vapidKey).toString();
153 const applicationServerKeyStr = new Uint8Array(
154 applicationServerKey,
155 ).toString();
156 const sameKey = vapidKeyStr === applicationServerKeyStr;
157 if (sameKey) {
158 // Subscription didn't change
159 } else {
160 // Subscription changed
161 console.error('🔔 Subscription changed', {
162 vapidKeyStr,
163 applicationServerKeyStr,
164 sameKey,
165 });
166 // Unsubscribe since backend doesn't have a subscription
167 await subscription.unsubscribe();
168 throw new Error('Subscription key and vapid key changed');
169 }
170 } else {
171 console.warn('No vapidKey found');
172 }
173 }
174
175 // Check if backend subscription returns 404
176 // if (subscription && !backendSubscription) {
177 // // Re-subscribe to backend
178 // backendSubscription = await createBackendPushSubscription({
179 // subscription,
180 // data: {},
181 // policy: 'all',
182 // });
183 // }
184
185 return { subscription, backendSubscription };
186}
187
188export async function updateSubscription({ data, policy }) {
189 console.log('🔔 Updating subscription', { data, policy });
190 if (!isPushSupported()) return;
191 let { registration, subscription } = await getSubscription();
192 let backendSubscription = null;
193
194 if (subscription) {
195 try {
196 backendSubscription = await updateBackendPushSubscription({
197 data,
198 policy,
199 });
200 // TODO: save subscription in user settings
201 } catch (error) {
202 // Backend doesn't have a subscription for this user
203 // Create a new one
204 backendSubscription = await createBackendPushSubscription({
205 subscription,
206 data,
207 policy,
208 });
209 // TODO: save subscription in user settings
210 }
211 } else {
212 // User is not subscribed
213 const vapidKey = getVapidKey();
214 if (!vapidKey) throw new Error('No server key found');
215 subscription = await registration.pushManager.subscribe({
216 userVisibleOnly: true,
217 applicationServerKey: urlBase64ToUint8Array(vapidKey),
218 });
219 backendSubscription = await createBackendPushSubscription({
220 subscription,
221 data,
222 policy,
223 });
224 // TODO: save subscription in user settings
225 }
226
227 return { subscription, backendSubscription };
228}
229
230export async function removeSubscription() {
231 if (!isPushSupported()) return;
232 const { subscription } = await getSubscription();
233 if (subscription) {
234 await removeBackendPushSubscription();
235 await subscription.unsubscribe();
236 }
237}