From d0211c8af1c07e5bd1493603132b8fe52969c24f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 15 Oct 2024 16:08:14 -0500 Subject: [PATCH] Reorganize functions in push registerer so they make logical sense --- src/actions/push-notifications/registerer.ts | 114 +++++++++---------- src/utils/base64.ts | 14 ++- 2 files changed, 68 insertions(+), 60 deletions(-) diff --git a/src/actions/push-notifications/registerer.ts b/src/actions/push-notifications/registerer.ts index 4a7df6a2b..f3e4d4a6f 100644 --- a/src/actions/push-notifications/registerer.ts +++ b/src/actions/push-notifications/registerer.ts @@ -2,67 +2,15 @@ import { HTTPError } from 'soapbox/api/HTTPError'; import { MastodonClient } from 'soapbox/api/MastodonClient'; import { WebPushSubscription, webPushSubscriptionSchema } from 'soapbox/schemas/web-push'; -import { decode as decodeBase64 } from 'soapbox/utils/base64'; - -/** Taken from https://www.npmjs.com/package/web-push */ -function urlBase64ToUint8Array(base64String: string): Uint8Array { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding) - .replace(/-/g, '+') - .replace(/_/g, '/'); - - return decodeBase64(base64); -} - -async function getBackendSubscription(api: MastodonClient): Promise { - try { - const response = await api.get('/api/v1/push/subscription'); - const data = await response.json(); - return webPushSubscriptionSchema.parse(data); - } catch (e) { - if (e instanceof HTTPError && e.response.status === 404) { - return null; - } else { - throw e; - } - } -} - -async function sendSubscriptionToBackend(api: MastodonClient, subscription: PushSubscription): Promise { - const params = { - subscription: subscription.toJSON(), - }; - - const response = await api.post('/api/v1/push/subscription', params); - const data = await response.json(); - - return webPushSubscriptionSchema.parse(data); -} - -async function createSubscription(vapidKey: string): Promise { - const registration = await navigator.serviceWorker.ready; - - return registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(vapidKey), - }); -} - -async function getOrCreateSubscription(vapidKey: string): Promise<{ subscription: PushSubscription; created: boolean }> { - const registration = await navigator.serviceWorker.ready; - const subscription = await registration.pushManager.getSubscription(); - - if (subscription) { - return { subscription, created: false }; - } else { - const subscription = await createSubscription(vapidKey); - return { subscription, created: true }; - } -} +import { decodeBase64Url } from 'soapbox/utils/base64'; // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); +/** + * Register web push notifications. + * This function creates a subscription if one hasn't been created already, and syncronizes it with the backend. + */ export async function registerPushNotifications(api: MastodonClient, vapidKey: string) { if (!supportsPushNotifications) { console.warn('Your browser does not support Web Push Notifications.'); @@ -91,6 +39,56 @@ export async function registerPushNotifications(api: MastodonClient, vapidKey: s } } +/** Get an existing subscription object from the browser if it exists, or ask the browser to create one. */ +async function getOrCreateSubscription(vapidKey: string): Promise<{ subscription: PushSubscription; created: boolean }> { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + + if (subscription) { + return { subscription, created: false }; + } else { + const subscription = await createSubscription(vapidKey); + return { subscription, created: true }; + } +} + +/** Request a subscription object from the web browser. */ +async function createSubscription(vapidKey: string): Promise { + const registration = await navigator.serviceWorker.ready; + + return registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: decodeBase64Url(vapidKey), + }); +} + +/** Fetch the API for an existing subscription saved in the backend, if any. */ +async function getBackendSubscription(api: MastodonClient): Promise { + try { + const response = await api.get('/api/v1/push/subscription'); + const data = await response.json(); + return webPushSubscriptionSchema.parse(data); + } catch (e) { + if (e instanceof HTTPError && e.response.status === 404) { + return null; + } else { + throw e; + } + } +} + +/** Publish a new subscription to the backend. */ +async function sendSubscriptionToBackend(api: MastodonClient, subscription: PushSubscription): Promise { + const params = { + subscription: subscription.toJSON(), + }; + + const response = await api.post('/api/v1/push/subscription', params); + const data = await response.json(); + + return webPushSubscriptionSchema.parse(data); +} + /** Check if the VAPID key and endpoint of the subscription match the data in the backend. */ function subscriptionMatchesBackend(subscription: PushSubscription, backend: WebPushSubscription): boolean { const { applicationServerKey } = subscription.options; @@ -103,7 +101,7 @@ function subscriptionMatchesBackend(subscription: PushSubscription, backend: Web return false; } - const backendKeyBytes: Uint8Array = urlBase64ToUint8Array(backend.server_key); + const backendKeyBytes: Uint8Array = decodeBase64Url(backend.server_key); const subscriptionKeyBytes: Uint8Array = new Uint8Array(applicationServerKey); return backendKeyBytes.toString() === subscriptionKeyBytes.toString(); diff --git a/src/utils/base64.ts b/src/utils/base64.ts index c512a6594..ebdf57ae8 100644 --- a/src/utils/base64.ts +++ b/src/utils/base64.ts @@ -1,4 +1,4 @@ -export const decode = (base64: string) => { +export function decodeBase64(base64: string): Uint8Array { const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); @@ -7,4 +7,14 @@ export const decode = (base64: string) => { } return outputArray; -}; +} + +/** Taken from https://www.npmjs.com/package/web-push */ +export function decodeBase64Url(base64String: string): Uint8Array { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/-/g, '+') + .replace(/_/g, '/'); + + return decodeBase64(base64); +} \ No newline at end of file