Created
May 5, 2025 09:15
-
-
Save agrittiwari/bad34f6cbf2bb65b2fd99b05eb2a3d93 to your computer and use it in GitHub Desktop.
Implementing Push Notifications on web using service worker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //backend code | |
| import webPush from "web-push"; | |
| import { Prisma } from "@prisma/client"; | |
| export type PushNotificationData = { | |
| to: string | string[]; | |
| webPushSub: Prisma.JsonValue | Prisma.JsonArray; | |
| title: string; | |
| body: string; | |
| data: { | |
| screen: string; | |
| params: Record<string, unknown>; | |
| }; | |
| }; | |
| // VAPID keys should be stored in environment variables | |
| const apiKeys = { | |
| publicKey: process.env.WEB_PUSH_PUBLIC_KEY, | |
| privateKey: process.env.WEB_PUSH_PRIVATE_KEY, | |
| }; | |
| // Set VAPID details (replace mailto with your own admin email if needed) | |
| webPush.setVapidDetails( | |
| "mailto:admin@example.com", | |
| apiKeys.publicKey!, | |
| apiKeys.privateKey! | |
| ); | |
| export const sendPushNotification = async ( | |
| data: PushNotificationData | |
| ): Promise<{ status: number; message: string }> => { | |
| try { | |
| // Append default sound setting or extend payload here if needed | |
| const updatedData = { ...data, sound: "default" }; | |
| // Utility function to send a single web push notification | |
| const sendWebPushNotification = async (subscription: any) => { | |
| try { | |
| await webPush.sendNotification(subscription, JSON.stringify(updatedData)); | |
| } catch (err) { | |
| console.error("Failed to send notification:", err); | |
| // Optionally integrate with an error tracking service here | |
| } | |
| }; | |
| // Support both single and multiple subscriptions | |
| if (Array.isArray(data.webPushSub)) { | |
| await Promise.all(data.webPushSub.map(sendWebPushNotification)); | |
| } else { | |
| await sendWebPushNotification(data.webPushSub); | |
| } | |
| return { | |
| status: 200, | |
| message: "Notifications sent successfully", | |
| }; | |
| } catch (err) { | |
| console.error("Error in sendPushNotification:", err); | |
| return { | |
| status: 500, | |
| message: "Failed to send push notifications", | |
| }; | |
| } | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Load Workbox from CDN | |
| importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js'); | |
| // Precache assets if Workbox is available | |
| if (workbox) { | |
| console.log('Workbox is loaded'); | |
| workbox.precaching.precacheAndRoute(self.__WB_MANIFEST); | |
| } else { | |
| console.log('Workbox failed to load'); | |
| } | |
| const graphqlEndpoint = 'https://your-api-endpoint.com/graphql'; // Replace with your GraphQL endpoint | |
| // Handle fetch to inject auth token | |
| self.addEventListener('fetch', (event) => { | |
| const url = new URL(event.request.url); | |
| if (url.pathname === '/graphql') { | |
| event.respondWith(handleRequest(event.request)); | |
| } | |
| }); | |
| // Activate event: subscribe to push notifications and send subscription to backend | |
| self.addEventListener('activate', async () => { | |
| try { | |
| const subscription = await self.registration.pushManager.subscribe({ | |
| userVisibleOnly: true, | |
| applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY'), | |
| }); | |
| const query = `mutation UpsertUser($webPushSub: JSON!) { | |
| updateUser(input: { webPushSubscription: $webPushSub }) { | |
| id | |
| webPushSubscription | |
| } | |
| }`; | |
| await savePushNotificationSubscriptionMutation(query, subscription, 'UpsertUser'); | |
| } catch (error) { | |
| console.error('Failed to subscribe to push notifications:', error); | |
| } | |
| }); | |
| // Display notification when a push event is received | |
| self.addEventListener('push', (event) => { | |
| const data = event.data ? event.data.json() : {}; | |
| const title = data.title || 'Hello!'; | |
| const options = { | |
| body: data.body || 'Default notification body', | |
| icon: data.icon || '/assets/icon.png', | |
| }; | |
| event.waitUntil(self.registration.showNotification(title, options)); | |
| }); | |
| // Handle GraphQL request with injected token | |
| async function handleRequest(request) { | |
| const modifiedRequest = new Request(request, { | |
| headers: new Headers(request.headers), | |
| }); | |
| const accessToken = await getAccessTokenFromIndexedDB(); | |
| if (accessToken) { | |
| modifiedRequest.headers.set('Authorization', `Bearer ${accessToken}`); | |
| } | |
| return fetch(modifiedRequest); | |
| } | |
| // Retrieve token from IndexedDB | |
| function getAccessTokenFromIndexedDB() { | |
| return new Promise((resolve, reject) => { | |
| const request = indexedDB.open('auth-db'); | |
| request.onerror = () => { | |
| console.error('Could not open IndexedDB'); | |
| reject(null); | |
| }; | |
| request.onsuccess = () => { | |
| const db = request.result; | |
| const transaction = db.transaction(['tokens'], 'readonly'); | |
| const store = transaction.objectStore('tokens'); | |
| const getRequest = store.get('accessToken'); | |
| getRequest.onsuccess = () => { | |
| resolve(getRequest.result ? getRequest.result.accessToken : null); | |
| }; | |
| getRequest.onerror = () => { | |
| console.error('Could not retrieve access token'); | |
| reject(null); | |
| }; | |
| }; | |
| }); | |
| } | |
| // Save subscription to backend | |
| async function savePushNotificationSubscriptionMutation(query, variables, operationName) { | |
| try { | |
| const accessToken = await getAccessTokenFromIndexedDB(); | |
| const response = await fetch(graphqlEndpoint, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${accessToken}`, | |
| }, | |
| body: JSON.stringify({ | |
| query, | |
| variables: { | |
| webPushSub: variables, | |
| }, | |
| operationName, | |
| }), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| console.log('GraphQL response:', data); | |
| } catch (error) { | |
| console.error('GraphQL error:', error); | |
| } | |
| } | |
| // VAPID key converter | |
| const urlBase64ToUint8Array = (base64String) => { | |
| const padding = '='.repeat((4 - (base64String.length % 4)) % 4); | |
| const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/'); | |
| const rawData = atob(base64); | |
| const outputArray = new Uint8Array(rawData.length); | |
| for (let i = 0; i < rawData.length; ++i) { | |
| outputArray[i] = rawData.charCodeAt(i); | |
| } | |
| return outputArray; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module.exports = { | |
| globDirectory: 'public/', | |
| globPatterns: ['**/*.{js,css,html,png,svg}'], | |
| swSrc: 'public/service-worker.js', | |
| swDest: 'public/sw.js', | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment