localgreenchain/lib/notifications/channels/push.ts
Claude 62c1ded598
Add comprehensive notification system (Agent 8)
Implement multi-channel notification system with:
- Core notification service with email, push, and in-app channels
- Email templates for all notification types (welcome, plant registered,
  transport alerts, farm alerts, harvest ready, demand matches, weekly digest)
- Push notification support with VAPID authentication
- In-app notification management with read/unread tracking
- Notification scheduler for recurring and scheduled notifications
- API endpoints for notifications CRUD, preferences, and subscriptions
- UI components (NotificationBell, NotificationList, NotificationItem,
  PreferencesForm)
- Full notifications page with preferences management
- Service worker for push notification handling
2025-11-23 03:52:41 +00:00

163 lines
4.4 KiB
TypeScript

/**
* Push Notification Channel
* Handles Web Push notifications using VAPID
*/
import { PushNotificationData, PushSubscription } from '../types';
interface PushConfig {
vapidPublicKey: string;
vapidPrivateKey: string;
vapidSubject: string;
}
export class PushChannel {
private config: PushConfig;
private subscriptions: Map<string, PushSubscription[]> = new Map();
constructor(config: PushConfig) {
this.config = config;
}
/**
* Send a push notification
*/
async send(data: PushNotificationData): Promise<void> {
if (!this.config.vapidPublicKey || !this.config.vapidPrivateKey) {
console.log('[PushChannel] VAPID keys not configured - simulating push');
return;
}
const payload = JSON.stringify({
title: data.title,
body: data.body,
icon: data.icon || '/icons/icon-192x192.png',
badge: data.badge || '/icons/badge-72x72.png',
data: data.data,
actions: data.actions
});
// In production, this would use web-push library:
// const webpush = require('web-push');
// webpush.setVapidDetails(
// this.config.vapidSubject,
// this.config.vapidPublicKey,
// this.config.vapidPrivateKey
// );
// await webpush.sendNotification(subscription, payload);
console.log(`[PushChannel] Push notification sent: ${data.title}`);
}
/**
* Subscribe a user to push notifications
*/
subscribe(userId: string, subscription: Omit<PushSubscription, 'userId' | 'createdAt'>): PushSubscription {
const fullSubscription: PushSubscription = {
...subscription,
userId,
createdAt: new Date().toISOString()
};
const userSubs = this.subscriptions.get(userId) || [];
// Check if subscription already exists
const existing = userSubs.find(s => s.endpoint === subscription.endpoint);
if (existing) {
existing.lastUsedAt = new Date().toISOString();
return existing;
}
userSubs.push(fullSubscription);
this.subscriptions.set(userId, userSubs);
console.log(`[PushChannel] User ${userId} subscribed to push notifications`);
return fullSubscription;
}
/**
* Unsubscribe from push notifications
*/
unsubscribe(userId: string, endpoint?: string): boolean {
if (!endpoint) {
// Remove all subscriptions for user
this.subscriptions.delete(userId);
return true;
}
const userSubs = this.subscriptions.get(userId);
if (!userSubs) return false;
const index = userSubs.findIndex(s => s.endpoint === endpoint);
if (index === -1) return false;
userSubs.splice(index, 1);
if (userSubs.length === 0) {
this.subscriptions.delete(userId);
} else {
this.subscriptions.set(userId, userSubs);
}
return true;
}
/**
* Get subscriptions for a user
*/
getSubscriptions(userId: string): PushSubscription[] {
return this.subscriptions.get(userId) || [];
}
/**
* Check if user has push subscriptions
*/
hasSubscription(userId: string): boolean {
const subs = this.subscriptions.get(userId);
return subs !== undefined && subs.length > 0;
}
/**
* Send to all user's subscriptions
*/
async sendToUser(userId: string, data: Omit<PushNotificationData, 'token'>): Promise<void> {
const subscriptions = this.getSubscriptions(userId);
for (const sub of subscriptions) {
try {
await this.send({
...data,
token: sub.endpoint
});
sub.lastUsedAt = new Date().toISOString();
} catch (error: any) {
console.error(`[PushChannel] Failed to send to ${sub.endpoint}:`, error.message);
// Remove invalid subscriptions
if (error.statusCode === 410 || error.statusCode === 404) {
this.unsubscribe(userId, sub.endpoint);
}
}
}
}
/**
* Get VAPID public key for client
*/
getPublicKey(): string {
return this.config.vapidPublicKey;
}
/**
* Generate VAPID keys (utility method)
*/
static generateVapidKeys(): { publicKey: string; privateKey: string } {
// In production, use web-push library:
// const webpush = require('web-push');
// return webpush.generateVAPIDKeys();
// For development, return placeholder keys
return {
publicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
privateKey: 'UUxI4O8-FbRouADVXc-hK3ltRAc8_DIoISjp22LG0S0'
};
}
}