1
0
Fork 0
mirror of https://github.com/imjasonh/webpush synced 2026-07-06 23:52:25 +00:00

Use go:embed for static content and simplify README to reference example

Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-24 22:28:44 +00:00
parent 41f2c224ec
commit 191144839a
4 changed files with 265 additions and 631 deletions

425
README.md
View file

@ -22,36 +22,22 @@ go get github.com/imjasonh/webpush
## Quick Start
### 1. Generate VAPID Keys
See the [example/](example/) directory for a complete working demo with:
- VAPID key generation and storage on disk
- SQLite subscription storage
- Web client with service worker for push notifications
- Automatic periodic notifications and manual `/ping` endpoint
```go
package main
To run the example:
import (
"fmt"
"github.com/imjasonh/webpush/keys"
)
func main() {
// Generate a new key pair and get base64-encoded keys
privateKey, publicKey, err := keys.GenerateKeyPair()
if err != nil {
panic(err)
}
fmt.Println("Private Key:", privateKey)
fmt.Println("Public Key:", publicKey)
// Or generate and save to a PEM file
signer, err := keys.GenerateKey("vapid-private.pem")
if err != nil {
panic(err)
}
fmt.Println("Public Key:", signer.PublicKeyBase64())
}
```bash
cd example
go run main.go
```
### 2. Send Push Notifications
Then open http://localhost:8080 in your browser.
## Basic Usage
```go
package main
@ -63,9 +49,8 @@ import (
)
func main() {
// Load VAPID keys
// Load or generate VAPID keys
signer, err := keys.NewFileSigner("vapid-private.pem")
// Or from base64: signer, err := keys.NewFileSignerFromBase64("your-base64-private-key")
if err != nil {
panic(err)
}
@ -73,7 +58,7 @@ func main() {
// Create client with VAPID subject (must be mailto: or https: URL)
client := webpush.NewClient(signer, "mailto:admin@example.com")
// Parse subscription from client
// Parse subscription from client (received from browser)
sub, err := webpush.ParseSubscription([]byte(`{
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
@ -87,9 +72,8 @@ func main() {
// Send notification
err = client.Send(context.Background(), sub, []byte(`{"title":"Hello","body":"World"}`), &webpush.Options{
TTL: 3600, // Time-to-live in seconds
Urgency: "high", // very-low, low, normal, high
Topic: "updates", // For message collapsing
TTL: 3600,
Urgency: "high",
})
if err != nil {
panic(err)
@ -97,97 +81,24 @@ func main() {
}
```
### 3. Store Subscriptions
```go
package main
import (
"context"
"github.com/google/uuid"
"github.com/imjasonh/webpush"
"github.com/imjasonh/webpush/storage"
)
func main() {
// Use SQLite for production
store, err := storage.NewSQLite("subscriptions.db")
// Or use in-memory for testing: store := storage.NewMemory()
if err != nil {
panic(err)
}
defer store.Close()
// Save a subscription
sub := &webpush.Subscription{
Endpoint: "https://fcm.googleapis.com/fcm/send/...",
Keys: webpush.Keys{
P256dh: "...",
Auth: "...",
},
}
record := &storage.Record{
ID: uuid.New().String(),
UserID: "user-123", // Optional: associate with your user
Subscription: sub,
}
err = store.Save(context.Background(), record)
if err != nil {
panic(err)
}
// Get subscriptions for a user
records, err := store.GetByUserID(context.Background(), "user-123")
if err != nil {
panic(err)
}
// Send to all user's devices
for _, r := range records {
// client.Send(ctx, r.Subscription, payload, opts)
}
}
```
## Using Google Cloud KMS
For production environments, you can store your VAPID private key in Google Cloud KMS:
```go
package main
import (
"context"
"github.com/imjasonh/webpush"
"github.com/imjasonh/webpush/keys"
)
func main() {
ctx := context.Background()
// Key name format: projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{key}/cryptoKeyVersions/{version}
keyName := "projects/my-project/locations/global/keyRings/webpush/cryptoKeys/vapid/cryptoKeyVersions/1"
signer, err := keys.NewKMSSigner(ctx, keyName)
if err != nil {
panic(err)
}
defer signer.Close()
client := webpush.NewClient(signer, "mailto:admin@example.com")
// Use client as normal...
signer, err := keys.NewKMSSigner(ctx, "projects/my-project/locations/global/keyRings/webpush/cryptoKeys/vapid/cryptoKeyVersions/1")
if err != nil {
panic(err)
}
defer signer.Close()
client := webpush.NewClient(signer, "mailto:admin@example.com")
```
### Creating a KMS Key
Create a KMS key:
```bash
# Create a key ring
gcloud kms keyrings create webpush --location=global
# Create an EC P-256 signing key
gcloud kms keys create vapid \
--keyring=webpush \
--location=global \
@ -195,287 +106,23 @@ gcloud kms keys create vapid \
--default-algorithm=ec-sign-p256-sha256
```
## JavaScript Client Integration
### Service Worker Setup
1. Create a service worker file (`sw.js`):
```javascript
self.addEventListener('push', function(event) {
const data = event.data ? event.data.json() : {};
const title = data.title || 'Notification';
const options = {
body: data.body || '',
icon: data.icon || '/icon.png',
badge: data.badge || '/badge.png',
data: data.data || {}
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
// Handle notification click
if (event.notification.data && event.notification.data.url) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
```
2. Register the service worker and subscribe to push:
```javascript
// Your VAPID public key from the server (base64 URL-safe encoded)
const VAPID_PUBLIC_KEY = 'YOUR_PUBLIC_KEY_HERE';
// Convert base64 to Uint8Array
function urlBase64ToUint8Array(base64String) {
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;
}
async function subscribeToNotifications() {
// Register service worker
const registration = await navigator.serviceWorker.register('/sw.js');
// Wait for service worker to be ready
await navigator.serviceWorker.ready;
// Subscribe to push notifications
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// Send subscription to your server
await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
console.log('Subscribed to push notifications');
}
// Request permission and subscribe
async function enableNotifications() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
await subscribeToNotifications();
}
}
```
3. Create a simple server endpoint to receive subscriptions:
```go
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/google/uuid"
"github.com/imjasonh/webpush"
"github.com/imjasonh/webpush/keys"
"github.com/imjasonh/webpush/storage"
)
var (
store storage.Storage
client *webpush.Client
)
func main() {
// Initialize storage
var err error
store, err = storage.NewSQLite("subscriptions.db")
if err != nil {
panic(err)
}
defer store.Close()
// Initialize VAPID signer
signer, err := keys.NewFileSigner("vapid-private.pem")
if err != nil {
panic(err)
}
client = webpush.NewClient(signer, "mailto:admin@example.com")
// Serve static files
http.Handle("/", http.FileServer(http.Dir("static")))
// API endpoints
http.HandleFunc("/api/vapid-public-key", handleVAPIDPublicKey)
http.HandleFunc("/api/subscribe", handleSubscribe)
http.HandleFunc("/api/push", handlePush)
http.ListenAndServe(":8080", nil)
}
func handleVAPIDPublicKey(w http.ResponseWriter, r *http.Request) {
// Return the public key for the JavaScript client
signer, _ := keys.NewFileSigner("vapid-private.pem")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"publicKey": signer.PublicKeyBase64(),
})
}
func handleSubscribe(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var sub webpush.Subscription
if err := json.NewDecoder(r.Body).Decode(&sub); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Validate subscription
if sub.Endpoint == "" || sub.Keys.P256dh == "" || sub.Keys.Auth == "" {
http.Error(w, "Invalid subscription", http.StatusBadRequest)
return
}
// Check if subscription already exists
existing, err := store.GetByEndpoint(r.Context(), sub.Endpoint)
if err == nil {
// Already subscribed
json.NewEncoder(w).Encode(map[string]string{"id": existing.ID})
return
}
// Save new subscription
record := &storage.Record{
ID: uuid.New().String(),
Subscription: &sub,
}
if err := store.Save(r.Context(), record); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": record.ID})
}
func handlePush(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Title string `json:"title"`
Body string `json:"body"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
payload, _ := json.Marshal(req)
// Get all subscriptions
records, err := store.List(r.Context(), 1000, 0)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Send to all subscriptions
ctx := context.Background()
var sent, failed int
for _, record := range records {
err := client.Send(ctx, record.Subscription, payload, nil)
if err != nil {
failed++
// If subscription is gone (410), delete it
// if strings.Contains(err.Error(), "410") {
// store.Delete(ctx, record.ID)
// }
} else {
sent++
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]int{
"sent": sent,
"failed": failed,
})
}
```
## API Reference
### Types
#### `Subscription`
```go
type Subscription struct {
Endpoint string `json:"endpoint"`
Keys Keys `json:"keys"`
}
type Keys struct {
P256dh string `json:"p256dh"`
Auth string `json:"auth"`
}
```
#### `Options`
```go
type Options struct {
TTL int // Time-to-live in seconds (default: 2419200 = 4 weeks)
Urgency string // Urgency: very-low, low, normal, high
Urgency string // very-low, low, normal, high
Topic string // Topic for message replacement
}
```
### Client
```go
// Create a new client
client := webpush.NewClient(signer, subject)
// Optionally set custom HTTP client
client.WithHTTPClient(httpClient)
// Send a notification
err := client.Send(ctx, subscription, payload, options)
// Parse subscription from JSON
sub, err := webpush.ParseSubscription(jsonBytes)
```
### Key Providers
```go
@ -487,7 +134,6 @@ signer, err := keys.NewFileSignerFromBase64("base64-private-key")
// Google Cloud KMS
signer, err := keys.NewKMSSigner(ctx, "projects/.../cryptoKeyVersions/1")
defer signer.Close()
// Generate new keys
signer, err := keys.GenerateKey("path/to/save.pem")
@ -502,19 +148,20 @@ store := storage.NewMemory()
// SQLite
store, err := storage.NewSQLite("subscriptions.db")
defer store.Close()
// Operations
err := store.Save(ctx, record)
record, err := store.Get(ctx, id)
record, err := store.GetByEndpoint(ctx, endpoint)
records, err := store.GetByUserID(ctx, userID)
records, err := store.List(ctx, limit, offset)
err := store.Delete(ctx, id)
err := store.DeleteByEndpoint(ctx, endpoint)
store.Save(ctx, record)
store.Get(ctx, id)
store.GetByEndpoint(ctx, endpoint)
store.GetByUserID(ctx, userID)
store.List(ctx, limit, offset)
store.Delete(ctx, id)
store.DeleteByEndpoint(ctx, endpoint)
```
## Custom Storage Implementation
## Custom Implementations
### Custom Storage
Implement the `storage.Storage` interface:
@ -531,7 +178,7 @@ type Storage interface {
}
```
## Custom Key Provider
### Custom Key Provider
Implement the `webpush.Signer` interface:
@ -542,7 +189,7 @@ type Signer interface {
}
```
The `Sign` method receives SHA-256 hash and should return IEEE P1363 format signature (64 bytes for P-256).
The `Sign` method receives a SHA-256 hash and should return an IEEE P1363 format signature (64 bytes for P-256).
## License

View file

@ -10,7 +10,9 @@ package main
import (
"context"
"embed"
"encoding/json"
"io/fs"
"log"
"net/http"
"os"
@ -24,6 +26,9 @@ import (
"github.com/imjasonh/webpush/storage"
)
//go:embed static/*
var staticFiles embed.FS
const (
keyPath = "vapid-private.pem"
dbPath = "subscriptions.db"
@ -74,8 +79,11 @@ func main() {
go periodicPush()
// Set up HTTP handlers
http.HandleFunc("/", handleIndex)
http.HandleFunc("/sw.js", handleServiceWorker)
staticFS, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatalf("Failed to create static file system: %v", err)
}
http.Handle("/", http.FileServer(http.FS(staticFS)))
http.HandleFunc("/api/vapid-public-key", handleVAPIDPublicKey)
http.HandleFunc("/api/subscribe", handleSubscribe)
http.HandleFunc("/api/unsubscribe", handleUnsubscribe)
@ -153,20 +161,6 @@ func isGone(err error) bool {
// HTTP Handlers
func handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(indexHTML))
}
func handleServiceWorker(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
w.Write([]byte(serviceWorkerJS))
}
func handleVAPIDPublicKey(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
@ -268,229 +262,3 @@ func handlePing(w http.ResponseWriter, r *http.Request) {
"message": "Push notification queued",
})
}
// Static content
const indexHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Push Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
h1 { color: #333; }
button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 5px;
}
button:hover { background: #0056b3; }
button:disabled { background: #ccc; cursor: not-allowed; }
button.danger { background: #dc3545; }
button.danger:hover { background: #c82333; }
button.success { background: #28a745; }
#status {
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.info { background: #cce5ff; color: #004085; }
</style>
</head>
<body>
<div class="card">
<h1>🔔 Web Push Demo</h1>
<p>Subscribe to receive push notifications from this server.</p>
<p>Notifications are sent:</p>
<ul>
<li>Every minute automatically</li>
<li>When someone visits <a href="/ping">/ping</a></li>
</ul>
</div>
<div class="card">
<h2>Subscription</h2>
<button id="subscribeBtn" onclick="subscribe()">Subscribe to Notifications</button>
<button id="unsubscribeBtn" onclick="unsubscribe()" class="danger" disabled>Unsubscribe</button>
<div id="status"></div>
</div>
<div class="card">
<h2>Test Push</h2>
<button onclick="sendPing()" class="success">Send Ping to All Subscribers</button>
</div>
<script>
let vapidPublicKey = '';
let currentSubscription = null;
// Convert base64 to Uint8Array
function urlBase64ToUint8Array(base64String) {
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 setStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = type;
}
async function init() {
// Get VAPID public key
const resp = await fetch('/api/vapid-public-key');
const data = await resp.json();
vapidPublicKey = data.publicKey;
// Check if already subscribed
if ('serviceWorker' in navigator && 'PushManager' in window) {
const registration = await navigator.serviceWorker.register('/sw.js');
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
currentSubscription = subscription;
document.getElementById('subscribeBtn').disabled = true;
document.getElementById('unsubscribeBtn').disabled = false;
setStatus('You are subscribed to notifications', 'success');
}
} else {
setStatus('Push notifications not supported in this browser', 'error');
document.getElementById('subscribeBtn').disabled = true;
}
}
async function subscribe() {
try {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
setStatus('Notification permission denied', 'error');
return;
}
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
});
// Send to server
const resp = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription.toJSON())
});
if (resp.ok) {
currentSubscription = subscription;
document.getElementById('subscribeBtn').disabled = true;
document.getElementById('unsubscribeBtn').disabled = false;
setStatus('Successfully subscribed!', 'success');
} else {
setStatus('Failed to subscribe: ' + await resp.text(), 'error');
}
} catch (err) {
setStatus('Error: ' + err.message, 'error');
}
}
async function unsubscribe() {
try {
if (currentSubscription) {
await fetch('/api/unsubscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ endpoint: currentSubscription.endpoint })
});
await currentSubscription.unsubscribe();
currentSubscription = null;
}
document.getElementById('subscribeBtn').disabled = false;
document.getElementById('unsubscribeBtn').disabled = true;
setStatus('Successfully unsubscribed', 'info');
} catch (err) {
setStatus('Error: ' + err.message, 'error');
}
}
async function sendPing() {
try {
const resp = await fetch('/ping');
if (resp.ok) {
setStatus('Ping sent to all subscribers!', 'success');
} else {
setStatus('Failed to send ping', 'error');
}
} catch (err) {
setStatus('Error: ' + err.message, 'error');
}
}
init();
</script>
</body>
</html>
`
const serviceWorkerJS = `
self.addEventListener('push', function(event) {
let data = { title: 'Notification', body: '' };
if (event.data) {
try {
data = event.data.json();
} catch (e) {
data.body = event.data.text();
}
}
const options = {
body: data.body || '',
icon: data.icon || '',
badge: data.badge || '',
data: data.data || {},
requireInteraction: false
};
event.waitUntil(
self.registration.showNotification(data.title || 'Notification', options)
);
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
if (event.notification.data && event.notification.data.url) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
`

186
example/static/index.html Normal file
View file

@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Push Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
h1 { color: #333; }
button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 5px;
}
button:hover { background: #0056b3; }
button:disabled { background: #ccc; cursor: not-allowed; }
button.danger { background: #dc3545; }
button.danger:hover { background: #c82333; }
button.success { background: #28a745; }
#status {
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.info { background: #cce5ff; color: #004085; }
</style>
</head>
<body>
<div class="card">
<h1>🔔 Web Push Demo</h1>
<p>Subscribe to receive push notifications from this server.</p>
<p>Notifications are sent:</p>
<ul>
<li>Every minute automatically</li>
<li>When someone visits <a href="/ping">/ping</a></li>
</ul>
</div>
<div class="card">
<h2>Subscription</h2>
<button id="subscribeBtn" onclick="subscribe()">Subscribe to Notifications</button>
<button id="unsubscribeBtn" onclick="unsubscribe()" class="danger" disabled>Unsubscribe</button>
<div id="status"></div>
</div>
<div class="card">
<h2>Test Push</h2>
<button onclick="sendPing()" class="success">Send Ping to All Subscribers</button>
</div>
<script>
let vapidPublicKey = '';
let currentSubscription = null;
// Convert base64 to Uint8Array
function urlBase64ToUint8Array(base64String) {
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 setStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = type;
}
async function init() {
// Get VAPID public key
const resp = await fetch('/api/vapid-public-key');
const data = await resp.json();
vapidPublicKey = data.publicKey;
// Check if already subscribed
if ('serviceWorker' in navigator && 'PushManager' in window) {
const registration = await navigator.serviceWorker.register('/sw.js');
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
currentSubscription = subscription;
document.getElementById('subscribeBtn').disabled = true;
document.getElementById('unsubscribeBtn').disabled = false;
setStatus('You are subscribed to notifications', 'success');
}
} else {
setStatus('Push notifications not supported in this browser', 'error');
document.getElementById('subscribeBtn').disabled = true;
}
}
async function subscribe() {
try {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
setStatus('Notification permission denied', 'error');
return;
}
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
});
// Send to server
const resp = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription.toJSON())
});
if (resp.ok) {
currentSubscription = subscription;
document.getElementById('subscribeBtn').disabled = true;
document.getElementById('unsubscribeBtn').disabled = false;
setStatus('Successfully subscribed!', 'success');
} else {
setStatus('Failed to subscribe: ' + await resp.text(), 'error');
}
} catch (err) {
setStatus('Error: ' + err.message, 'error');
}
}
async function unsubscribe() {
try {
if (currentSubscription) {
await fetch('/api/unsubscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ endpoint: currentSubscription.endpoint })
});
await currentSubscription.unsubscribe();
currentSubscription = null;
}
document.getElementById('subscribeBtn').disabled = false;
document.getElementById('unsubscribeBtn').disabled = true;
setStatus('Successfully unsubscribed', 'info');
} catch (err) {
setStatus('Error: ' + err.message, 'error');
}
}
async function sendPing() {
try {
const resp = await fetch('/ping');
if (resp.ok) {
setStatus('Ping sent to all subscribers!', 'success');
} else {
setStatus('Failed to send ping', 'error');
}
} catch (err) {
setStatus('Error: ' + err.message, 'error');
}
}
init();
</script>
</body>
</html>

33
example/static/sw.js Normal file
View file

@ -0,0 +1,33 @@
self.addEventListener('push', function(event) {
let data = { title: 'Notification', body: '' };
if (event.data) {
try {
data = event.data.json();
} catch (e) {
data.body = event.data.text();
}
}
const options = {
body: data.body || '',
icon: data.icon || '',
badge: data.badge || '',
data: data.data || {},
requireInteraction: false
};
event.waitUntil(
self.registration.showNotification(data.title || 'Notification', options)
);
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
if (event.notification.data && event.notification.data.url) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});