137 lines
5 KiB
TypeScript
137 lines
5 KiB
TypeScript
import type { mastodon } from 'masto'
|
|
import type {
|
|
CreatePushNotification,
|
|
PushManagerSubscriptionInfo,
|
|
RequiredUserLogin,
|
|
} from '~/composables/push-notifications/types'
|
|
import { PushSubscriptionError } from '~/composables/push-notifications/types'
|
|
|
|
export const createPushSubscription = async (
|
|
user: RequiredUserLogin,
|
|
notificationData: CreatePushNotification,
|
|
policy: mastodon.v1.SubscriptionPolicy = 'all',
|
|
force = false,
|
|
): Promise<mastodon.v1.WebPushSubscription | undefined> => {
|
|
const { server: serverEndpoint, vapidKey } = user
|
|
|
|
return await getRegistration()
|
|
.then(getPushSubscription)
|
|
.then(({ registration, subscription }): Promise<mastodon.v1.WebPushSubscription | undefined> => {
|
|
if (subscription) {
|
|
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey!)).toString()
|
|
const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString()
|
|
|
|
// If the VAPID public key did not change and the endpoint corresponds
|
|
// to the endpoint saved in the backend, the subscription is valid
|
|
// If push subscription is not there, we need to create it: it is fetched on login
|
|
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint && (!force && user.pushSubscription)) {
|
|
return Promise.resolve(user.pushSubscription)
|
|
}
|
|
else if (user.pushSubscription) {
|
|
// if we have a subscription, but it is not valid or forcing renew, we need to remove it
|
|
// we need to prevent removing push notification data
|
|
return unsubscribeFromBackend(false, false)
|
|
.catch(removePushNotificationDataOnError)
|
|
.then(() => subscribe(registration, vapidKey))
|
|
.then(subscription => sendSubscriptionToBackend(subscription, notificationData, policy))
|
|
}
|
|
}
|
|
|
|
return subscribe(registration, vapidKey).then(
|
|
subscription => sendSubscriptionToBackend(subscription, notificationData, policy),
|
|
)
|
|
})
|
|
.catch((error) => {
|
|
let useError: PushSubscriptionError | Error = error
|
|
if (error.code === 11 && error.name === 'InvalidStateError')
|
|
useError = new PushSubscriptionError('too_many_registrations', 'Too many registrations')
|
|
else if (error.code === 20 && error.name === 'AbortError')
|
|
useError = new PushSubscriptionError('vapid_not_supported', 'Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.')
|
|
else if (error.code === 5 && error.name === 'InvalidCharacterError')
|
|
useError = new PushSubscriptionError('invalid_vapid_key', `The VAPID public key seems to be invalid: ${vapidKey}`)
|
|
|
|
return getRegistration()
|
|
.then(getPushSubscription)
|
|
.then(() => unsubscribeFromBackend(true))
|
|
.then(() => Promise.resolve(undefined))
|
|
.catch((e) => {
|
|
console.error(e)
|
|
return Promise.resolve(undefined)
|
|
})
|
|
.finally(() => {
|
|
return Promise.reject(useError)
|
|
})
|
|
})
|
|
}
|
|
|
|
// Taken from https://www.npmjs.com/package/web-push
|
|
function urlBase64ToUint8Array(base64String: string) {
|
|
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
|
const base64 = `${base64String}${padding}`
|
|
.replace(/-/g, '+')
|
|
.replace(/_/g, '/')
|
|
|
|
const rawData = window.atob(base64)
|
|
const outputArray = new Uint8Array(rawData.length)
|
|
|
|
for (let i = 0; i < rawData.length; ++i)
|
|
outputArray[i] = rawData.charCodeAt(i)
|
|
|
|
return outputArray
|
|
}
|
|
|
|
function getRegistration() {
|
|
return navigator.serviceWorker.ready
|
|
}
|
|
async function getPushSubscription(registration: ServiceWorkerRegistration): Promise<PushManagerSubscriptionInfo> {
|
|
const subscription = await registration.pushManager.getSubscription()
|
|
return { registration, subscription }
|
|
}
|
|
|
|
async function subscribe(
|
|
registration: ServiceWorkerRegistration,
|
|
applicationServerKey: string,
|
|
): Promise<PushSubscription> {
|
|
return await registration.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
|
|
})
|
|
}
|
|
|
|
async function unsubscribeFromBackend(fromSWPushManager: boolean, removePushNotification = true) {
|
|
const cu = currentUser.value
|
|
if (cu) {
|
|
await removePushNotifications(cu)
|
|
removePushNotification && await removePushNotificationData(cu, fromSWPushManager)
|
|
}
|
|
}
|
|
|
|
async function removePushNotificationDataOnError(e: Error) {
|
|
const cu = currentUser.value
|
|
if (cu)
|
|
await removePushNotificationData(cu, true)
|
|
|
|
throw e
|
|
}
|
|
|
|
async function sendSubscriptionToBackend(
|
|
subscription: PushSubscription,
|
|
data: CreatePushNotification,
|
|
policy: mastodon.v1.SubscriptionPolicy,
|
|
): Promise<mastodon.v1.WebPushSubscription> {
|
|
const { endpoint, keys } = subscription.toJSON()
|
|
const params: mastodon.v1.CreateWebPushSubscriptionParams = {
|
|
policy,
|
|
subscription: {
|
|
endpoint: endpoint!,
|
|
keys: {
|
|
p256dh: keys!.p256dh!,
|
|
auth: keys!.auth!,
|
|
},
|
|
},
|
|
data,
|
|
}
|
|
|
|
return await useMastoClient().v1.webPushSubscriptions.create(params)
|
|
}
|