localgreenchain/lib/notifications/channels/email.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

358 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Email Notification Channel
* Handles sending email notifications via SMTP or SendGrid
*/
import { EmailNotificationData, NotificationPayload, NotificationType } from '../types';
interface EmailConfig {
provider: 'sendgrid' | 'nodemailer' | 'smtp';
apiKey?: string;
from: string;
replyTo?: string;
smtp?: {
host: string;
port: number;
secure: boolean;
user: string;
pass: string;
};
}
export class EmailChannel {
private config: EmailConfig;
constructor(config: EmailConfig) {
this.config = config;
}
/**
* Send an email notification
*/
async send(data: EmailNotificationData): Promise<void> {
const emailData = {
...data,
from: data.from || this.config.from,
replyTo: data.replyTo || this.config.replyTo
};
switch (this.config.provider) {
case 'sendgrid':
await this.sendViaSendGrid(emailData);
break;
case 'smtp':
case 'nodemailer':
await this.sendViaSMTP(emailData);
break;
default:
// Development mode - log email
console.log('[EmailChannel] Development mode - Email would be sent:', {
to: emailData.to,
subject: emailData.subject,
preview: emailData.text?.substring(0, 100)
});
}
}
/**
* Send via SendGrid API
*/
private async sendViaSendGrid(data: EmailNotificationData): Promise<void> {
if (!this.config.apiKey) {
throw new Error('SendGrid API key not configured');
}
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
personalizations: [{ to: [{ email: data.to }] }],
from: { email: data.from },
reply_to: data.replyTo ? { email: data.replyTo } : undefined,
subject: data.subject,
content: [
{ type: 'text/plain', value: data.text || data.html.replace(/<[^>]*>/g, '') },
{ type: 'text/html', value: data.html }
]
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`SendGrid error: ${error}`);
}
}
/**
* Send via SMTP (using nodemailer-like approach)
*/
private async sendViaSMTP(data: EmailNotificationData): Promise<void> {
// In production, this would use nodemailer
// For now, we'll simulate the SMTP send
if (!this.config.smtp?.host) {
console.log('[EmailChannel] SMTP not configured - simulating send');
return;
}
// Simulate SMTP connection and send
console.log(`[EmailChannel] Sending email via SMTP to ${data.to}`);
// In production implementation:
// const nodemailer = require('nodemailer');
// const transporter = nodemailer.createTransport({
// host: this.config.smtp.host,
// port: this.config.smtp.port,
// secure: this.config.smtp.secure,
// auth: {
// user: this.config.smtp.user,
// pass: this.config.smtp.pass
// }
// });
// await transporter.sendMail(data);
}
/**
* Render email template based on notification type
*/
async renderTemplate(payload: NotificationPayload): Promise<string> {
const templates = {
welcome: this.getWelcomeTemplate,
plant_registered: this.getPlantRegisteredTemplate,
plant_reminder: this.getPlantReminderTemplate,
transport_alert: this.getTransportAlertTemplate,
farm_alert: this.getFarmAlertTemplate,
harvest_ready: this.getHarvestReadyTemplate,
demand_match: this.getDemandMatchTemplate,
weekly_digest: this.getWeeklyDigestTemplate,
system_alert: this.getSystemAlertTemplate
};
const templateFn = templates[payload.type] || this.getDefaultTemplate;
return templateFn.call(this, payload);
}
/**
* Base email layout
*/
private getBaseLayout(content: string, payload: NotificationPayload): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${payload.title}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f5f5f5; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.header h1 { margin: 0; font-size: 24px; }
.logo { font-size: 32px; margin-bottom: 10px; }
.content { background: white; padding: 30px; border-radius: 0 0 8px 8px; }
.button { display: inline-block; background: #22c55e; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 20px 0; }
.button:hover { background: #16a34a; }
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
.alert-info { background: #dbeafe; border-left: 4px solid #3b82f6; padding: 15px; margin: 15px 0; }
.alert-warning { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 15px 0; }
.alert-success { background: #d1fae5; border-left: 4px solid #22c55e; padding: 15px; margin: 15px 0; }
.stats { display: flex; justify-content: space-around; margin: 20px 0; }
.stat-item { text-align: center; padding: 15px; }
.stat-value { font-size: 24px; font-weight: bold; color: #22c55e; }
.stat-label { font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">🌱</div>
<h1>LocalGreenChain</h1>
</div>
<div class="content">
${content}
</div>
<div class="footer">
<p>LocalGreenChain - Transparent Seed-to-Seed Tracking</p>
<p>
<a href="{{unsubscribe_url}}">Manage notification preferences</a>
</p>
</div>
</div>
</body>
</html>`;
}
private getWelcomeTemplate(payload: NotificationPayload): string {
const content = `
<h2>Welcome to LocalGreenChain! 🌿</h2>
<p>Thank you for joining our community of sustainable growers and conscious consumers.</p>
<p>With LocalGreenChain, you can:</p>
<ul>
<li>Track your plants from seed to seed</li>
<li>Monitor transport and carbon footprint</li>
<li>Connect with local growers and consumers</li>
<li>Manage vertical farms with precision</li>
</ul>
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">Get Started</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getPlantRegisteredTemplate(payload: NotificationPayload): string {
const data = payload.data || {};
const content = `
<h2>Plant Registered Successfully 🌱</h2>
<p>Your plant has been registered on the blockchain.</p>
<div class="alert-success">
<strong>Plant ID:</strong> ${data.plantId || 'N/A'}<br>
<strong>Species:</strong> ${data.species || 'N/A'}<br>
<strong>Variety:</strong> ${data.variety || 'N/A'}
</div>
<p>You can now track this plant throughout its entire lifecycle.</p>
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">View Plant Details</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getPlantReminderTemplate(payload: NotificationPayload): string {
const data = payload.data || {};
const content = `
<h2>Plant Care Reminder 🌿</h2>
<div class="alert-info">
<strong>${payload.title}</strong><br>
${payload.message}
</div>
${data.plantName ? `<p><strong>Plant:</strong> ${data.plantName}</p>` : ''}
${data.action ? `<p><strong>Recommended Action:</strong> ${data.action}</p>` : ''}
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">View Plant</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getTransportAlertTemplate(payload: NotificationPayload): string {
const data = payload.data || {};
const content = `
<h2>Transport Update 🚚</h2>
<div class="alert-info">
${payload.message}
</div>
${data.distance ? `
<div class="stats">
<div class="stat-item">
<div class="stat-value">${data.distance} km</div>
<div class="stat-label">Distance</div>
</div>
<div class="stat-item">
<div class="stat-value">${data.carbonKg || '0'} kg</div>
<div class="stat-label">Carbon Footprint</div>
</div>
</div>
` : ''}
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">View Journey</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getFarmAlertTemplate(payload: NotificationPayload): string {
const data = payload.data || {};
const alertClass = data.severity === 'warning' ? 'alert-warning' : 'alert-info';
const content = `
<h2>Farm Alert ${data.severity === 'warning' ? '⚠️' : ''}</h2>
<div class="${alertClass}">
<strong>${payload.title}</strong><br>
${payload.message}
</div>
${data.zone ? `<p><strong>Zone:</strong> ${data.zone}</p>` : ''}
${data.recommendation ? `<p><strong>Recommendation:</strong> ${data.recommendation}</p>` : ''}
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">View Farm Dashboard</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getHarvestReadyTemplate(payload: NotificationPayload): string {
const data = payload.data || {};
const content = `
<h2>Harvest Ready! 🎉</h2>
<div class="alert-success">
<strong>Great news!</strong> Your crop is ready for harvest.
</div>
${data.batchId ? `<p><strong>Batch:</strong> ${data.batchId}</p>` : ''}
${data.cropType ? `<p><strong>Crop:</strong> ${data.cropType}</p>` : ''}
${data.estimatedYield ? `<p><strong>Estimated Yield:</strong> ${data.estimatedYield}</p>` : ''}
<p>Log the harvest to update your blockchain records.</p>
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">Log Harvest</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getDemandMatchTemplate(payload: NotificationPayload): string {
const data = payload.data || {};
const content = `
<h2>Demand Match Found! 🤝</h2>
<p>We've found a match between supply and demand.</p>
<div class="alert-success">
${payload.message}
</div>
${data.matchDetails ? `
<p><strong>Crop:</strong> ${data.matchDetails.crop}</p>
<p><strong>Quantity:</strong> ${data.matchDetails.quantity}</p>
<p><strong>Region:</strong> ${data.matchDetails.region}</p>
` : ''}
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">View Match Details</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getWeeklyDigestTemplate(payload: NotificationPayload): string {
const data = payload.data || {};
const content = `
<h2>Your Weekly Summary 📊</h2>
<p>Here's what happened this week on LocalGreenChain:</p>
<div class="stats">
<div class="stat-item">
<div class="stat-value">${data.plantsRegistered || 0}</div>
<div class="stat-label">Plants Registered</div>
</div>
<div class="stat-item">
<div class="stat-value">${data.carbonSaved || 0} kg</div>
<div class="stat-label">Carbon Saved</div>
</div>
<div class="stat-item">
<div class="stat-value">${data.localMiles || 0}</div>
<div class="stat-label">Local Food Miles</div>
</div>
</div>
${data.highlights ? `
<h3>Highlights</h3>
<ul>
${data.highlights.map((h: string) => `<li>${h}</li>`).join('')}
</ul>
` : ''}
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">View Full Report</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getSystemAlertTemplate(payload: NotificationPayload): string {
const content = `
<h2>System Notification ⚙️</h2>
<div class="alert-info">
<strong>${payload.title}</strong><br>
${payload.message}
</div>
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">Learn More</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
private getDefaultTemplate(payload: NotificationPayload): string {
const content = `
<h2>${payload.title}</h2>
<p>${payload.message}</p>
${payload.actionUrl ? `<a href="${payload.actionUrl}" class="button">View Details</a>` : ''}
`;
return this.getBaseLayout(content, payload);
}
}