push-notifications.js
1 // Utils for push notifications 2 import { api } from './api'; 3 import { getCurrentAccount } 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 35 function createBackendPushSubscription(subscription) { 36 const { masto } = api(); 37 return masto.v1.push.subscription.create(subscription); 38 } 39 40 function fetchBackendPushSubscription() { 41 const { masto } = api(); 42 return masto.v1.push.subscription.fetch(); 43 } 44 45 function updateBackendPushSubscription(subscription) { 46 const { masto } = api(); 47 return masto.v1.push.subscription.update(subscription); 48 } 49 50 function removeBackendPushSubscription() { 51 const { masto } = api(); 52 return masto.v1.push.subscription.remove(); 53 } 54 55 // Front-end 56 // ========= 57 58 export function isPushSupported() { 59 return 'serviceWorker' in navigator && 'PushManager' in window; 60 } 61 62 export function getRegistration() { 63 // return navigator.serviceWorker.ready; 64 return navigator.serviceWorker.getRegistration(); 65 } 66 67 async function getSubscription() { 68 const registration = await getRegistration(); 69 const subscription = registration 70 ? await registration.pushManager.getSubscription() 71 : undefined; 72 return { registration, subscription }; 73 } 74 75 function 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 94 export 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 } = getCurrentAccount(); 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 } = getCurrentAccount(); 150 const { applicationServerKey } = subscription.options; 151 const vapidKeyStr = urlBase64ToUint8Array(vapidKey).toString(); 152 const applicationServerKeyStr = new Uint8Array( 153 applicationServerKey, 154 ).toString(); 155 const sameKey = vapidKeyStr === applicationServerKeyStr; 156 if (sameKey) { 157 // Subscription didn't change 158 } else { 159 // Subscription changed 160 console.error('🔔 Subscription changed', { 161 vapidKeyStr, 162 applicationServerKeyStr, 163 sameKey, 164 }); 165 // Unsubscribe since backend doesn't have a subscription 166 await subscription.unsubscribe(); 167 throw new Error('Subscription key and vapid key changed'); 168 } 169 } 170 171 // Check if backend subscription returns 404 172 // if (subscription && !backendSubscription) { 173 // // Re-subscribe to backend 174 // backendSubscription = await createBackendPushSubscription({ 175 // subscription, 176 // data: {}, 177 // policy: 'all', 178 // }); 179 // } 180 181 return { subscription, backendSubscription }; 182 } 183 184 export async function updateSubscription({ data, policy }) { 185 console.log('🔔 Updating subscription', { data, policy }); 186 if (!isPushSupported()) return; 187 let { registration, subscription } = await getSubscription(); 188 let backendSubscription = null; 189 190 if (subscription) { 191 try { 192 backendSubscription = await updateBackendPushSubscription({ 193 data, 194 policy, 195 }); 196 // TODO: save subscription in user settings 197 } catch (error) { 198 // Backend doesn't have a subscription for this user 199 // Create a new one 200 backendSubscription = await createBackendPushSubscription({ 201 subscription, 202 data, 203 policy, 204 }); 205 // TODO: save subscription in user settings 206 } 207 } else { 208 // User is not subscribed 209 const { vapidKey } = getCurrentAccount(); 210 if (!vapidKey) throw new Error('No server key found'); 211 subscription = await registration.pushManager.subscribe({ 212 userVisibleOnly: true, 213 applicationServerKey: urlBase64ToUint8Array(vapidKey), 214 }); 215 backendSubscription = await createBackendPushSubscription({ 216 subscription, 217 data, 218 policy, 219 }); 220 // TODO: save subscription in user settings 221 } 222 223 return { subscription, backendSubscription }; 224 } 225 226 export async function removeSubscription() { 227 if (!isPushSupported()) return; 228 const { subscription } = await getSubscription(); 229 if (subscription) { 230 await removeBackendPushSubscription(); 231 await subscription.unsubscribe(); 232 } 233 }