From bdbe932872a2a9364cccb5bc8ea5c79853ae14bd Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Thu, 13 Jun 2024 12:42:41 +0200 Subject: [PATCH] feat(pwa): :sparkles: register service worker --- islands/RegisterServiceWorker.tsx | 35 +++++++++++++++++++++-- routes/api/webpush/subscription.ts | 27 ++++++++++++++++++ routes/api/webpush/vapid.ts | 9 ++++++ src/webpush/mod.ts | 45 ++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 routes/api/webpush/subscription.ts create mode 100644 routes/api/webpush/vapid.ts create mode 100644 src/webpush/mod.ts diff --git a/islands/RegisterServiceWorker.tsx b/islands/RegisterServiceWorker.tsx index 04de658..9a50d7c 100644 --- a/islands/RegisterServiceWorker.tsx +++ b/islands/RegisterServiceWorker.tsx @@ -1,8 +1,39 @@ +import { requestApi } from '../src/utils.ts' + export default function RegisterServiceWorker() { if ('serviceWorker' in navigator) { - import('./StartServiceWorker.tsx').then((mod) => { + import('./StartServiceWorker.tsx').then(async (mod) => { const href = mod.default() - navigator.serviceWorker.register(href, { scope: '/', type: 'module' }) + const registration = await navigator.serviceWorker.register(href, { + scope: '/', + type: 'module', + }) + + // Notification.requestPermission().then((permission) => { + // if (permission !== 'granted') return + + // registration.showNotification('Notification permission granted', { + // body: 'Notification is ok.', + // }) + // }) + + const subscription = await (async () => { + const currentSubscription = await registration.pushManager + .getSubscription() + if (currentSubscription) return currentSubscription + + const applicationServerKey = await requestApi( + 'webpush/vapid', + 'GET', + ) + + return await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey, + }) + })() + + await requestApi('webpush/subscription', 'POST', subscription) }) } diff --git a/routes/api/webpush/subscription.ts b/routes/api/webpush/subscription.ts new file mode 100644 index 0000000..8155e41 --- /dev/null +++ b/routes/api/webpush/subscription.ts @@ -0,0 +1,27 @@ +import { Handlers } from '$fresh/server.ts' +import { respondApi } from '../../../src/utils.ts' + +export const handler: Handlers = { + async POST(request: Request) { + const subscription = await request.json() as PushSubscriptionJSON + saveSubscription(subscription) + return respondApi('success', 'ok') + }, +} + +function saveSubscription(subscription: PushSubscriptionJSON) { + const itemKey = 'webpush-subscription' + const subscriptions = JSON.parse( + localStorage.getItem(itemKey) ?? '[]', + ) as PushSubscriptionJSON[] + + // Prevent duplicate + const auth = subscription.keys?.auth + if (subscriptions.some((sub) => sub.keys?.auth === auth)) { + return + } + + // Store subscription + subscriptions.push(subscription) + localStorage.setItem(itemKey, JSON.stringify(subscriptions)) +} diff --git a/routes/api/webpush/vapid.ts b/routes/api/webpush/vapid.ts new file mode 100644 index 0000000..4abcfe5 --- /dev/null +++ b/routes/api/webpush/vapid.ts @@ -0,0 +1,9 @@ +import { Handlers } from '$fresh/server.ts' +import { respondApi } from '../../../src/utils.ts' +import { publicKey } from '../../../src/webpush/mod.ts' + +export const handler: Handlers = { + GET() { + return respondApi('success', publicKey) + }, +} diff --git a/src/webpush/mod.ts b/src/webpush/mod.ts new file mode 100644 index 0000000..51e101f --- /dev/null +++ b/src/webpush/mod.ts @@ -0,0 +1,45 @@ +//@deno-types="npm:@types/web-push@^3.6.3" +import webpush from 'web-push' + +// DEV mode +// localStorage.clear() + +const vapidKeys = getVapidKeys() +export const { publicKey } = vapidKeys + +webpush.setVapidDetails( + 'mailto:contact@example.com', + vapidKeys.publicKey, + vapidKeys.privateKey, +) + +// const itemKey = 'webpush-subscription' +// setInterval(async () => { +// const subscriptions = JSON.parse(localStorage.getItem(itemKey) ?? '[]') as PushSubscriptionJSON[] + +// for (const subscription of subscriptions) { +// try { +// // console.log('PUSH NOTIFICATION', subscription) +// const payload: { title: string, options?: Partial } = { title: `This is a test @ ${new Date().toLocaleTimeString()}` } +// //@ts-ignore TODO check endpoint is defined +// const a = await webpush.sendNotification(subscription, JSON.stringify(payload)) +// console.log(a) +// } catch (error) { +// console.error(error) +// } +// } +// }, 10_000) + +function getVapidKeys(): webpush.VapidKeys { + const itemKey = 'webpush-vapid-keys' + const item = localStorage.getItem(itemKey) + + if (item) { + const vapidKeys = JSON.parse(item) as webpush.VapidKeys + return vapidKeys + } + + const vapidKeys = webpush.generateVAPIDKeys() + localStorage.setItem(itemKey, JSON.stringify(vapidKeys)) + return vapidKeys +}