this repo has no description
at main 237 lines 6.7 kB view raw
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}