/ src / utils / push-notifications.js
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  }