├── .gitignore ├── jest.config.cjs ├── types └── index.d.ts ├── package.json ├── src └── index.js ├── tests └── pushmatic.test.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "jsdom", 3 | transform: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare class Pushmatic { 2 | static requestPermission(): Promise; 3 | static registerServiceWorker( 4 | scriptURL: string 5 | ): Promise; 6 | static subscribeToPush( 7 | registration: ServiceWorkerRegistration, 8 | options: PushSubscriptionOptionsInit 9 | ): Promise; 10 | static initializePushNotifications( 11 | serviceWorkerUrl: string, 12 | options: PushSubscriptionOptionsInit 13 | ): Promise; 14 | } 15 | 16 | export default Pushmatic; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pushmatic", 3 | "version": "1.0.1", 4 | "description": "A lightweight, framework-agnostic library to simplify web push notifications.", 5 | "main": "src/index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "types": "types/index.d.ts", 10 | "scripts": { 11 | "test": "NODE_OPTIONS='--experimental-vm-modules --experimental-specifier-resolution=node' jest" 12 | }, 13 | "author": "Mohamed Salah", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "jest": "^29.7.0", 17 | "jest-environment-jsdom": "^29.7.0" 18 | }, 19 | "type": "module", 20 | "keywords": [ 21 | "web-push", 22 | "notification", 23 | "push-notification", 24 | "web-push-notification", 25 | "push", 26 | "javascript", 27 | "typescript" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/mhmdsalahsebai/pushmatic.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/mhmdsalahsebai/pushmatic/issues" 35 | }, 36 | "homepage": "https://github.com/mhmdsalahsebai/pushmatic#README" 37 | } 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * pushmatic: A simple library to manage web push notifications. 3 | */ 4 | class Pushmatic { 5 | /** 6 | * Request permission for notifications. 7 | * @returns {Promise} Resolves to "granted" if successful. 8 | */ 9 | static requestPermission() { 10 | return new Promise((resolve, reject) => { 11 | if (!("Notification" in window)) { 12 | return reject( 13 | new Error("This browser does not support notifications.") 14 | ); 15 | } 16 | Notification.requestPermission().then((permission) => { 17 | if (permission === "granted") { 18 | resolve(permission); 19 | } else { 20 | reject(new Error("Notification permission not granted.")); 21 | } 22 | }); 23 | }); 24 | } 25 | 26 | /** 27 | * Register a service worker. 28 | * @param {string} scriptURL - The URL of the service worker script. 29 | * @returns {Promise} The registration object. 30 | */ 31 | static registerServiceWorker(scriptURL) { 32 | return new Promise((resolve, reject) => { 33 | if (!("serviceWorker" in navigator)) { 34 | return reject( 35 | new Error("Service Workers are not supported in this browser.") 36 | ); 37 | } 38 | navigator.serviceWorker 39 | .register(scriptURL) 40 | .then((registration) => resolve(registration)) 41 | .catch(reject); 42 | }); 43 | } 44 | 45 | /** 46 | * Subscribe to push notifications. 47 | * @param {ServiceWorkerRegistration} registration - The service worker registration object. 48 | * @param {PushSubscriptionOptionsInit} options - The subscription options. 49 | * @returns {Promise} The push subscription object. 50 | */ 51 | static subscribeToPush(registration, options) { 52 | return new Promise((resolve, reject) => { 53 | if (!registration.pushManager) { 54 | return reject( 55 | new Error("Push messaging is not supported in this browser.") 56 | ); 57 | } 58 | if (Notification.permission !== "granted") { 59 | return reject(new Error("Notification permission is not granted.")); 60 | } 61 | registration.pushManager.subscribe(options).then(resolve).catch(reject); 62 | }); 63 | } 64 | 65 | /** 66 | * Initialize push notifications with a single function. 67 | * @param {string} serviceWorkerUrl - The URL of the service worker script. 68 | * @param {PushSubscriptionOptionsInit} options - Push subscription options. 69 | * @returns {Promise} Resolves with the push subscription data. 70 | */ 71 | static initializePushNotifications(serviceWorkerUrl, options) { 72 | return this.registerServiceWorker(serviceWorkerUrl) 73 | .then((registration) => { 74 | return this.requestPermission().then(() => registration); 75 | }) 76 | .then((registration) => this.subscribeToPush(registration, options)); 77 | } 78 | } 79 | 80 | export default Pushmatic; 81 | -------------------------------------------------------------------------------- /tests/pushmatic.test.js: -------------------------------------------------------------------------------- 1 | import Pushmatic from "../src"; 2 | import { jest } from "@jest/globals"; 3 | 4 | describe("Pushmatic Library", () => { 5 | /* --------------------------- 6 | Notification Permission Tests 7 | ---------------------------- */ 8 | describe("requestPermission", () => { 9 | beforeAll(() => { 10 | // Create a global Notification object to simulate the browser API. 11 | global.Notification = { 12 | requestPermission: jest.fn(), 13 | permission: "default", 14 | }; 15 | }); 16 | 17 | beforeEach(() => { 18 | Notification.requestPermission.mockReset(); 19 | Notification.permission = "default"; 20 | }); 21 | 22 | test("should resolve when permission is granted", async () => { 23 | Notification.requestPermission.mockResolvedValue("granted"); 24 | const permission = await Pushmatic.requestPermission(); 25 | expect(permission).toBe("granted"); 26 | }); 27 | 28 | test("should reject when permission is not granted", async () => { 29 | Notification.requestPermission.mockResolvedValue("denied"); 30 | await expect(Pushmatic.requestPermission()).rejects.toThrow( 31 | "Notification permission not granted." 32 | ); 33 | }); 34 | }); 35 | 36 | /* --------------------------- 37 | Service Worker Registration Tests 38 | ---------------------------- */ 39 | describe("registerServiceWorker", () => { 40 | let originalServiceWorker; 41 | 42 | beforeAll(() => { 43 | originalServiceWorker = navigator.serviceWorker; 44 | }); 45 | 46 | afterEach(() => { 47 | navigator.serviceWorker = originalServiceWorker; 48 | }); 49 | 50 | test("should register a service worker and resolve with registration", async () => { 51 | const dummyRegistration = { pushManager: {} }; 52 | navigator.serviceWorker = { 53 | register: jest.fn().mockResolvedValue(dummyRegistration), 54 | }; 55 | 56 | const registration = await Pushmatic.registerServiceWorker("/sw.js"); 57 | expect(navigator.serviceWorker.register).toHaveBeenCalledWith("/sw.js"); 58 | expect(registration).toEqual(dummyRegistration); 59 | }); 60 | 61 | test("should reject if Service Workers are not supported", async () => { 62 | delete global.navigator.serviceWorker; 63 | await expect(Pushmatic.registerServiceWorker("/sw.js")).rejects.toThrow( 64 | "Service Workers are not supported in this browser." 65 | ); 66 | }); 67 | 68 | test("should reject when service worker registration fails", async () => { 69 | const error = new Error("Registration failed"); 70 | navigator.serviceWorker = { 71 | register: jest.fn().mockRejectedValue(error), 72 | }; 73 | await expect(Pushmatic.registerServiceWorker("/sw.js")).rejects.toThrow( 74 | "Registration failed" 75 | ); 76 | }); 77 | }); 78 | 79 | /* --------------------------- 80 | Push Subscription Tests 81 | ---------------------------- */ 82 | describe("subscribeToPush", () => { 83 | let dummyRegistration; 84 | let dummyPushManager; 85 | 86 | beforeEach(() => { 87 | Notification.permission = "granted"; 88 | 89 | dummyPushManager = { 90 | subscribe: jest.fn(), 91 | }; 92 | 93 | dummyRegistration = { 94 | pushManager: dummyPushManager, 95 | }; 96 | }); 97 | 98 | test("should subscribe to push notifications and resolve with subscription data", async () => { 99 | const dummySubscription = { endpoint: "https://dummy.push/subscription" }; 100 | dummyPushManager.subscribe.mockResolvedValue(dummySubscription); 101 | 102 | const options = { 103 | userVisibleOnly: true, 104 | applicationServerKey: "dummyKey", 105 | }; 106 | const subscription = await Pushmatic.subscribeToPush( 107 | dummyRegistration, 108 | options 109 | ); 110 | expect(dummyPushManager.subscribe).toHaveBeenCalledWith(options); 111 | expect(subscription).toEqual(dummySubscription); 112 | }); 113 | 114 | test("should reject if pushManager is not supported", async () => { 115 | const regWithoutPushManager = {}; 116 | const options = { 117 | userVisibleOnly: true, 118 | applicationServerKey: "dummyKey", 119 | }; 120 | await expect( 121 | Pushmatic.subscribeToPush(regWithoutPushManager, options) 122 | ).rejects.toThrow("Push messaging is not supported in this browser."); 123 | }); 124 | 125 | test("should reject if Notification permission is not granted", async () => { 126 | Notification.permission = "default"; 127 | const options = { 128 | userVisibleOnly: true, 129 | applicationServerKey: "dummyKey", 130 | }; 131 | await expect( 132 | Pushmatic.subscribeToPush(dummyRegistration, options) 133 | ).rejects.toThrow("Notification permission is not granted."); 134 | }); 135 | 136 | test("should reject when push subscription fails", async () => { 137 | const error = new Error("Subscription failed"); 138 | dummyPushManager.subscribe.mockRejectedValue(error); 139 | const options = { 140 | userVisibleOnly: true, 141 | applicationServerKey: "dummyKey", 142 | }; 143 | await expect( 144 | Pushmatic.subscribeToPush(dummyRegistration, options) 145 | ).rejects.toThrow("Subscription failed"); 146 | }); 147 | }); 148 | 149 | /* --------------------------- 150 | Combined Initialization Tests 151 | ---------------------------- */ 152 | describe("initializePushNotifications", () => { 153 | let originalRequestPermission; 154 | let originalRegisterServiceWorker; 155 | let originalSubscribeToPush; 156 | 157 | beforeEach(() => { 158 | originalRequestPermission = Pushmatic.requestPermission; 159 | originalRegisterServiceWorker = Pushmatic.registerServiceWorker; 160 | originalSubscribeToPush = Pushmatic.subscribeToPush; 161 | 162 | Object.defineProperty(global.navigator, "serviceWorker", { 163 | value: { 164 | register: jest.fn().mockResolvedValue({ pushManager: {} }), 165 | }, 166 | writable: true, 167 | }); 168 | }); 169 | 170 | afterEach(() => { 171 | Pushmatic.requestPermission = originalRequestPermission; 172 | Pushmatic.registerServiceWorker = originalRegisterServiceWorker; 173 | Pushmatic.subscribeToPush = originalSubscribeToPush; 174 | }); 175 | 176 | test("should resolve with subscription data when all steps succeed", async () => { 177 | Pushmatic.requestPermission = jest.fn().mockResolvedValue("granted"); 178 | const dummyRegistration = { pushManager: {} }; 179 | Pushmatic.registerServiceWorker = jest 180 | .fn() 181 | .mockResolvedValue(dummyRegistration); 182 | const dummySubscription = { endpoint: "https://dummy.push/subscription" }; 183 | Pushmatic.subscribeToPush = jest 184 | .fn() 185 | .mockResolvedValue(dummySubscription); 186 | 187 | const options = { 188 | userVisibleOnly: true, 189 | applicationServerKey: "dummyKey", 190 | }; 191 | const subscription = await Pushmatic.initializePushNotifications( 192 | "/sw.js", 193 | options 194 | ); 195 | 196 | expect(Pushmatic.requestPermission).toHaveBeenCalled(); 197 | expect(Pushmatic.registerServiceWorker).toHaveBeenCalledWith("/sw.js"); 198 | expect(Pushmatic.subscribeToPush).toHaveBeenCalledWith( 199 | dummyRegistration, 200 | options 201 | ); 202 | expect(subscription).toEqual(dummySubscription); 203 | }); 204 | 205 | test("should reject if any step fails (simulate requestPermission failure)", async () => { 206 | jest.spyOn(Pushmatic, "registerServiceWorker").mockResolvedValue({ 207 | pushManager: {}, 208 | }); 209 | 210 | jest 211 | .spyOn(Pushmatic, "requestPermission") 212 | .mockRejectedValue(new Error("Permission error")); 213 | 214 | await expect( 215 | Pushmatic.initializePushNotifications("/sw.js", { 216 | userVisibleOnly: true, 217 | applicationServerKey: "dummyKey", 218 | }) 219 | ).rejects.toThrow("Permission error"); 220 | }); 221 | 222 | test("should reject if any step fails (simulate service worker registration failure)", async () => { 223 | Pushmatic.requestPermission = jest.fn().mockResolvedValue("granted"); 224 | Pushmatic.registerServiceWorker = jest 225 | .fn() 226 | .mockRejectedValue(new Error("Registration error")); 227 | const options = { 228 | userVisibleOnly: true, 229 | applicationServerKey: "dummyKey", 230 | }; 231 | await expect( 232 | Pushmatic.initializePushNotifications("/sw.js", options) 233 | ).rejects.toThrow("Registration error"); 234 | }); 235 | 236 | test("should reject if any step fails (simulate subscribeToPush failure)", async () => { 237 | const dummyRegistration = { pushManager: {} }; 238 | Pushmatic.requestPermission = jest.fn().mockResolvedValue("granted"); 239 | Pushmatic.registerServiceWorker = jest 240 | .fn() 241 | .mockResolvedValue(dummyRegistration); 242 | Pushmatic.subscribeToPush = jest 243 | .fn() 244 | .mockRejectedValue(new Error("Subscription error")); 245 | const options = { 246 | userVisibleOnly: true, 247 | applicationServerKey: "dummyKey", 248 | }; 249 | await expect( 250 | Pushmatic.initializePushNotifications("/sw.js", options) 251 | ).rejects.toThrow("Subscription error"); 252 | }); 253 | }); 254 | }); 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pushmatic 2 | 3 | A lightweight, framework-agnostic library for handling web push notifications easily. 4 | 5 | ## 🚀 Why Pushmatic? 6 | 7 | Web push notifications allow you to engage users even when they are not actively using your website. **Pushmatic** simplifies handling web push notifications. 8 | 9 | This package ensures **consistent behavior across all browsers** and handles browser-specific differences. For example: 10 | 11 | - **Service Worker First**: Pushmatic ensures that the service worker is registered **before** requesting notification permission, which is required in **Edge**. 12 | - **User-Generated Events**: It encourages requesting permission inside a **short-lived user-generated event** (e.g., a button click) because browsers like **Firefox** and **Safari** enforce this for security and user experience reasons. 13 | 14 | Pushmatic has been **tested across all browsers that support web push notifications**, ensuring reliability and seamless integration. 15 | 16 | ### Web Push notifications 17 | 18 | Web push notifications is actually pretty simple: when a user chooses to allow browser notifications from your website, Microsoft/Mozilla/Google create a REST endpoint somewhere in their clouds. You just need to issue a signed POST request to that endpoint with a certain payload to have notifications pushed to the user’s browser. You can send as many requests as you like, and it’s completely free. 19 | 20 | Many developers get confused about this due to misleading articles suggesting that you need Firebase. However, since the Web Push protocol was standardized in 2016, all major browsers support it directly, eliminating the need for Firebase. 21 | 22 | **Good read on this topic:** [How to send web push notifications for free without Firebase](https://levelup.gitconnected.com/how-to-send-web-push-notifications-for-free-with-aws-and-without-firebase-19d02eadf1f7) 23 | 24 | ## ✨ Features 25 | 26 | - 📬 Subscribe users to push notifications 27 | - 🔍 Check push notification permission status 28 | - ✅ Unified API to handle everything in one function 29 | - ⚡ Promise-based API for easy integration 30 | - 🔗 Framework-agnostic (works with vanilla JS, React, Vue, etc.) 31 | - 🌍 Consistent behavior across all browsers 32 | 33 | ## 📦 Installation 34 | 35 | ```sh 36 | npm install pushmatic 37 | ``` 38 | 39 | ## 🌍 Live Demo & Source Code 40 | 41 | - Live Demo: https://pushmatic.vercel.app/ 42 | 43 | - Live Demo Source Code: https://github.com/mhmdsalahsebai/pushmatic-website 44 | 45 | ## 🔑 Generating and Using VAPID Keys 46 | 47 | To generate VAPID keys, run: 48 | 49 | ```sh 50 | npx web-push generate-vapid-keys --json 51 | ``` 52 | 53 | Example output: 54 | 55 | ``` 56 | Public Key: 57 | Your generated public key 58 | 59 | Private Key: 60 | Your generated private key 61 | ``` 62 | 63 | - The **public key** is used on the client-side and can be stored in an environment variable. 64 | - ⚠️ **Never store the private key in the client-side code!** The private key must remain secret and should only be used on your server to send push notifications. 65 | 66 | ## 🚀 One Function to Rule Them All 67 | 68 | ```js 69 | import Pushmatic from "pushmatic"; 70 | 71 | Pushmatic.initializePushNotifications("/sw.js", { 72 | userVisibleOnly: true, 73 | applicationServerKey: "YOUR_PUBLIC_VAPID_KEY", 74 | }) 75 | .then((subscription) => { 76 | console.log("Subscribed:", subscription); 77 | // Send subscription to you server 78 | }) 79 | .catch(console.error); 80 | ``` 81 | 82 | ## 🛠 API Reference 83 | 84 | ### `Pushmatic.initializePushNotifications(serviceWorkerUrl, options)` 85 | 86 | ✅ **Description**: 87 | Registers a service worker, requests notification permission, and subscribes to push notifications in a single call. 88 | 89 | ✅ **Parameters**: 90 | 91 | - `serviceWorkerUrl: string` → Path to the service worker file. 92 | - `options: PushSubscriptionOptionsInit` → Push subscription options (must include `userVisibleOnly: true` and your `applicationServerKey`). 93 | 94 | ✅ **Returns**: 95 | 96 | - `Promise` - Resolves with the subscription data. 97 | 98 | --- 99 | 100 | ## 🎯 Individual Methods (Modular Approach) 101 | 102 | If you prefer more control, you can use the individual methods instead of `initializePushNotifications()`. 103 | 104 | ### 1️⃣ `Pushmatic.requestPermission()` 105 | 106 | ✅ **Description**: 107 | Requests permission from the user to enable push notifications. 108 | 109 | ✅ **Returns**: 110 | 111 | - `Promise` - Resolves with `"granted"` if permission is allowed. 112 | - Rejects with an error if permission is denied. 113 | 114 | ✅ **Example**: 115 | 116 | ```js 117 | Pushmatic.requestPermission() 118 | .then((status) => console.log("Permission status:", status)) 119 | .catch((error) => console.error("Permission error:", error)); 120 | ``` 121 | 122 | --- 123 | 124 | ### 2️⃣ `Pushmatic.registerServiceWorker(scriptURL)` 125 | 126 | ✅ **Description**: 127 | Registers a service worker with the given script URL. 128 | 129 | ✅ **Parameters**: 130 | 131 | - `scriptURL: string` → The path to your service worker file (e.g., `"/sw.js"`). 132 | 133 | ✅ **Returns**: 134 | 135 | - `Promise` - Resolves with the service worker registration. 136 | 137 | ✅ **Example**: 138 | 139 | ```js 140 | Pushmatic.registerServiceWorker("/sw.js") 141 | .then((registration) => 142 | console.log("Service worker registered:", registration) 143 | ) 144 | .catch((error) => console.error("Service worker error:", error)); 145 | ``` 146 | 147 | --- 148 | 149 | ### 3️⃣ `Pushmatic.subscribeToPush(registration, options)` 150 | 151 | ✅ **Description**: 152 | Subscribes the user to push notifications using the given service worker registration. 153 | 154 | ✅ **Parameters**: 155 | 156 | - `registration: ServiceWorkerRegistration` → The service worker registration object. 157 | - `options: PushSubscriptionOptionsInit` → Push subscription options (must include `userVisibleOnly` and `applicationServerKey`). 158 | 159 | ✅ **Returns**: 160 | 161 | - `Promise` - Resolves with the push subscription data. 162 | 163 | ✅ **Example**: 164 | 165 | ```js 166 | const options = { 167 | userVisibleOnly: true, 168 | applicationServerKey: "your-public-key-here", 169 | }; 170 | 171 | Pushmatic.registerServiceWorker("/sw.js") 172 | .then((registration) => Pushmatic.subscribeToPush(registration, options)) 173 | .then((subscription) => console.log("Push subscription:", subscription)) 174 | .catch((error) => console.error("Push subscription error:", error)); 175 | ``` 176 | 177 | ## 📍 Where to Place the Service Worker 178 | 179 | Your service worker file (e.g., `sw.js`) should be placed at the root of your website so that it can control the entire domain. Example content for `sw.js`: 180 | 181 | ```js 182 | self.addEventListener("push", function (event) { 183 | const data = event.data.json(); 184 | self.registration.showNotification(data.title, { 185 | body: data.body, 186 | icon: "/icon.png", 187 | }); 188 | }); 189 | ``` 190 | 191 | 📌 At this point, Pushmatic library has completed its role. The next sections serve as tutorial. 192 | 193 | ## ⚛️ Using Pushmatic in a React Component 194 | 195 | ```jsx 196 | import { useState } from "react"; 197 | import Pushmatic from "pushmatic"; 198 | 199 | function PushButton() { 200 | const [subscribed, setSubscribed] = useState(false); 201 | 202 | function handleSubscribe() { 203 | Pushmatic.initializePushNotifications("/sw.js", { 204 | userVisibleOnly: true, 205 | applicationServerKey: "YOUR_PUBLIC_VAPID_KEY", 206 | }) 207 | .then((subscription) => { 208 | console.log("Subscribed:", subscription); 209 | setSubscribed(true); 210 | sendSubscriptionToBackEnd(subscription); 211 | }) 212 | .catch(console.error); 213 | } 214 | 215 | function sendSubscriptionToBackEnd(subscription) { 216 | return fetch("/api/save-subscription/", { 217 | method: "POST", 218 | headers: { 219 | "Content-Type": "application/json", 220 | }, 221 | body: JSON.stringify(subscription), 222 | }) 223 | .then((response) => { 224 | if (!response.ok) { 225 | throw new Error("Bad status code from server."); 226 | } 227 | return response.json(); 228 | }) 229 | .then((responseData) => { 230 | if (!(responseData.data && responseData.data.success)) { 231 | throw new Error("Bad response from server."); 232 | } 233 | }); 234 | } 235 | 236 | return ( 237 | 240 | ); 241 | } 242 | 243 | export default PushButton; 244 | ``` 245 | 246 | ⚠️ **Some browsers (like Firefox and Safari) require that the notification permission request be made inside a short-lived user-generated event (e.g., a button click). Otherwise, the request will be blocked.** 247 | 248 | ## 🌐 Using Pushmatic in Vanilla JavaScript 249 | 250 | ```html 251 | 252 | 253 | 267 | ``` 268 | 269 | ## 🌍 Server-Side: Saving Subscriptions and Sending Push Notifications 270 | 271 | To send notifications from your server, use the [`web-push`](https://www.npmjs.com/package/web-push) library. 272 | 273 | ### Install `web-push` on your backend 274 | 275 | ```sh 276 | npm install web-push 277 | ``` 278 | 279 | ### Storing User Subscriptions 280 | 281 | Your backend should have an endpoint to save user subscriptions somewhere (e.g., a database): 282 | 283 | ```js 284 | const express = require("express"); 285 | const app = express(); 286 | app.use(express.json()); 287 | 288 | const subscriptions = []; 289 | 290 | app.post("/api/save-subscription/", (req, res) => { 291 | const subscription = req.body; 292 | subscriptions.push(subscription); 293 | res.status(201).json({ data: { success: true } }); 294 | }); 295 | 296 | app.listen(3000, () => console.log("Server running on port 3000")); 297 | ``` 298 | 299 | ### Sending a Push Notification 300 | 301 | ```js 302 | const webpush = require("web-push"); 303 | 304 | const vapidKeys = { 305 | publicKey: "Your generated public key", 306 | privateKey: "Your generated private key", 307 | }; 308 | 309 | webpush.setVapidDetails( 310 | "mailto:your-email@example.com", 311 | vapidKeys.publicKey, 312 | vapidKeys.privateKey 313 | ); 314 | 315 | const payload = JSON.stringify({ 316 | title: "Hello!", 317 | body: "This is a push notification.", 318 | }); 319 | 320 | subscriptions.forEach((subscription) => { 321 | webpush 322 | .sendNotification(subscription, payload) 323 | .then(() => console.log("Push notification sent!")) 324 | .catch(console.error); 325 | }); 326 | ``` 327 | 328 | 🔗 **More details**: [Sending messages with Web Push libraries](https://web.dev/articles/sending-messages-with-web-push-libraries) 329 | 330 | ## 🧪 Testing 331 | 332 | Pushmatic uses [Jest](https://jestjs.io/) for unit testing. To run tests: 333 | 334 | ```sh 335 | npm test 336 | ``` 337 | 338 | ## 🛠 Contributing 339 | 340 | Contributions are welcome! Feel free to open issues or submit pull requests. 341 | 342 | ## 📜 License 343 | 344 | MIT License © Mohamed Salah 345 | --------------------------------------------------------------------------------