← back to Watches
push-notifications.js
298 lines
/**
* Push Notification Module for Omega Watches PWA
* Handles subscription management and sending push notifications
*/
import webpush from 'web-push';
// VAPID keys for push notifications
// In production, generate with: webpush generate-vapid-keys
const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY || 'YOUR_PUBLIC_KEY_HERE';
const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY || 'YOUR_PRIVATE_KEY_HERE';
const VAPID_SUBJECT = process.env.VAPID_SUBJECT || 'mailto:admin@omegawatches.example.com';
// Configure web-push
if (VAPID_PUBLIC_KEY !== 'YOUR_PUBLIC_KEY_HERE') {
webpush.setVapidDetails(
VAPID_SUBJECT,
VAPID_PUBLIC_KEY,
VAPID_PRIVATE_KEY
);
}
// In-memory storage for subscriptions (use database in production)
const subscriptions = new Map();
/**
* Save push subscription
*/
export function saveSubscription(userId, subscription) {
if (!userId || !subscription) {
throw new Error('userId and subscription are required');
}
subscriptions.set(userId, {
subscription,
createdAt: Date.now(),
lastUsed: Date.now()
});
console.log('[PushNotifications] Subscription saved for user:', userId);
return true;
}
/**
* Get subscription for user
*/
export function getSubscription(userId) {
const data = subscriptions.get(userId);
return data ? data.subscription : null;
}
/**
* Remove subscription
*/
export function removeSubscription(userId) {
const removed = subscriptions.delete(userId);
console.log('[PushNotifications] Subscription removed:', userId);
return removed;
}
/**
* Get all subscriptions
*/
export function getAllSubscriptions() {
return Array.from(subscriptions.values()).map(data => data.subscription);
}
/**
* Send push notification to specific user
*/
export async function sendNotificationToUser(userId, payload) {
const subscription = getSubscription(userId);
if (!subscription) {
throw new Error('No subscription found for user');
}
try {
const result = await webpush.sendNotification(
subscription,
JSON.stringify(payload)
);
// Update last used timestamp
const data = subscriptions.get(userId);
if (data) {
data.lastUsed = Date.now();
}
console.log('[PushNotifications] Notification sent to:', userId);
return result;
} catch (error) {
console.error('[PushNotifications] Send error:', error);
// Remove invalid subscriptions
if (error.statusCode === 410 || error.statusCode === 404) {
removeSubscription(userId);
}
throw error;
}
}
/**
* Send push notification to all subscribed users
*/
export async function sendNotificationToAll(payload) {
const allSubscriptions = getAllSubscriptions();
if (allSubscriptions.length === 0) {
console.warn('[PushNotifications] No subscriptions to send to');
return { sent: 0, failed: 0 };
}
const results = {
sent: 0,
failed: 0,
errors: []
};
const promises = allSubscriptions.map(async (subscription) => {
try {
await webpush.sendNotification(
subscription,
JSON.stringify(payload)
);
results.sent++;
} catch (error) {
results.failed++;
results.errors.push(error.message);
}
});
await Promise.all(promises);
console.log('[PushNotifications] Broadcast complete:', results);
return results;
}
/**
* Send price alert notification
*/
export async function sendPriceAlert(userId, watchData, priceChange) {
const payload = {
title: 'Price Alert!',
body: `${watchData.model}: ${priceChange > 0 ? '+' : ''}${priceChange}%`,
icon: '/icons/icon-192x192.svg',
badge: '/icons/icon-72x72.svg',
tag: `price-alert-${watchData.id}`,
requireInteraction: true,
data: {
url: `/pwa-index.html?watch=${watchData.id}`,
watchId: watchData.id,
change: priceChange
},
actions: [
{ action: 'view', title: 'View Details' },
{ action: 'dismiss', title: 'Dismiss' }
]
};
return sendNotificationToUser(userId, payload);
}
/**
* Send new watch notification
*/
export async function sendNewWatchNotification(watchData) {
const payload = {
title: 'New Watch Added',
body: `${watchData.series}: ${watchData.model}`,
icon: '/icons/icon-192x192.svg',
badge: '/icons/icon-72x72.svg',
tag: `new-watch-${watchData.id}`,
data: {
url: `/pwa-index.html?watch=${watchData.id}`,
watchId: watchData.id
}
};
return sendNotificationToAll(payload);
}
/**
* Send trending watch notification
*/
export async function sendTrendingNotification(watchData, reason) {
const payload = {
title: 'Trending Watch',
body: `${watchData.model} is trending! ${reason}`,
icon: '/icons/icon-192x192.svg',
badge: '/icons/icon-72x72.svg',
tag: `trending-${watchData.id}`,
data: {
url: `/pwa-index.html?watch=${watchData.id}`,
watchId: watchData.id
}
};
return sendNotificationToAll(payload);
}
/**
* Send custom notification
*/
export async function sendCustomNotification(userId, title, body, options = {}) {
const payload = {
title,
body,
icon: options.icon || '/icons/icon-192x192.svg',
badge: options.badge || '/icons/icon-72x72.svg',
tag: options.tag || 'custom-notification',
data: options.data || {},
...options
};
if (userId === 'all') {
return sendNotificationToAll(payload);
} else {
return sendNotificationToUser(userId, payload);
}
}
/**
* Get subscription statistics
*/
export function getSubscriptionStats() {
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const oneWeek = 7 * oneDay;
let active = 0;
let inactive = 0;
for (const [userId, data] of subscriptions.entries()) {
const daysSinceLastUsed = (now - data.lastUsed) / oneDay;
if (daysSinceLastUsed <= 7) {
active++;
} else {
inactive++;
}
}
return {
total: subscriptions.size,
active,
inactive,
subscriptions: Array.from(subscriptions.keys())
};
}
/**
* Clean up old subscriptions
*/
export function cleanupOldSubscriptions(daysOld = 90) {
const cutoff = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
let removed = 0;
for (const [userId, data] of subscriptions.entries()) {
if (data.lastUsed < cutoff) {
subscriptions.delete(userId);
removed++;
}
}
console.log(`[PushNotifications] Cleaned up ${removed} old subscriptions`);
return removed;
}
/**
* Generate VAPID keys (utility function)
*/
export function generateVapidKeys() {
const keys = webpush.generateVAPIDKeys();
return {
publicKey: keys.publicKey,
privateKey: keys.privateKey
};
}
export default {
saveSubscription,
getSubscription,
removeSubscription,
getAllSubscriptions,
sendNotificationToUser,
sendNotificationToAll,
sendPriceAlert,
sendNewWatchNotification,
sendTrendingNotification,
sendCustomNotification,
getSubscriptionStats,
cleanupOldSubscriptions,
generateVapidKeys,
VAPID_PUBLIC_KEY
};