- Add notification router (sendEmail, sendPush, sendSMS, device mgmt, prefs) - Create provider clients: Resend, Firebase Admin (FCM), Twilio - Add notification_preferences table to Drizzle schema - Create branded email templates (welcome, alert, password reset, family invite, billing) - Implement notification service with error handling and E.164 validation - Wire router into app root - Write unit tests with mocked providers (25 tests passing) - Add resend, firebase-admin, twilio dependencies
168 lines
7.1 KiB
TypeScript
168 lines
7.1 KiB
TypeScript
function brandedWrapper(title: string, body: string) {
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head><meta charset="utf-8"></head>
|
|
<body style="margin:0;padding:0;background-color:#f4f4f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f4;padding:24px 16px">
|
|
<tr>
|
|
<td align="center">
|
|
<table width="480" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:8px;overflow:hidden">
|
|
<tr>
|
|
<td style="background-color:#1a1a2e;padding:24px;text-align:center">
|
|
<h1 style="color:#ffffff;margin:0;font-size:20px;font-weight:700">🛡️ ShieldAI</h1>
|
|
<p style="color:#94a3b8;margin:4px 0 0;font-size:13px">Intelligent Protection</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:32px 24px">
|
|
<h2 style="color:#1a1a2e;margin:0 0 16px;font-size:18px">${title}</h2>
|
|
${body}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="background-color:#f8fafc;padding:16px 24px;text-align:center;border-top:1px solid #e2e8f0">
|
|
<p style="color:#64748b;margin:0;font-size:12px">ShieldAI — Your intelligent digital protection platform</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function brandedText(text: string) {
|
|
return `ShieldAI - Intelligent Protection\n\n${text}\n\n---\nShieldAI - Your intelligent digital protection platform`;
|
|
}
|
|
|
|
export interface EmailTemplate {
|
|
subject: string;
|
|
html: string;
|
|
text: string;
|
|
}
|
|
|
|
export function welcomeEmail(name: string): EmailTemplate {
|
|
return {
|
|
subject: "Welcome to ShieldAI",
|
|
html: brandedWrapper(
|
|
"Welcome to ShieldAI!",
|
|
`<p style="color:#334155;margin:0 0 12px;line-height:1.6">Hi ${name},</p>
|
|
<p style="color:#334155;margin:0 0 12px;line-height:1.6">Thank you for joining ShieldAI. We're here to help you monitor and protect your digital identity.</p>
|
|
<p style="color:#334155;margin:0 0 12px;line-height:1.6">Get started by adding your first watchlist item, and we'll alert you to any exposures or threats.</p>
|
|
<p style="color:#334155;margin:0;line-height:1.6">Stay safe,<br>The ShieldAI Team</p>`,
|
|
),
|
|
text: brandedText(
|
|
`Hi ${name},\n\nThank you for joining ShieldAI. We're here to help you monitor and protect your digital identity.\n\nGet started by adding your first watchlist item, and we'll alert you to any exposures or threats.\n\nStay safe,\nThe ShieldAI Team`,
|
|
),
|
|
};
|
|
}
|
|
|
|
export function alertNotificationEmail(
|
|
alertTitle: string,
|
|
alertMessage: string,
|
|
severity: string,
|
|
): EmailTemplate {
|
|
const severityColor =
|
|
severity === "critical" ? "#dc2626" :
|
|
severity === "warning" ? "#d97706" : "#2563eb";
|
|
|
|
return {
|
|
subject: `[${severity.toUpperCase()}] ShieldAI Alert: ${alertTitle}`,
|
|
html: brandedWrapper(
|
|
`Alert: ${alertTitle}`,
|
|
`<div style="display:inline-block;padding:4px 10px;border-radius:4px;font-size:12px;font-weight:600;text-transform:uppercase;color:#ffffff;background-color:${severityColor};margin-bottom:16px">${severity}</div>
|
|
<p style="color:#334155;margin:0 0 12px;line-height:1.6">${alertMessage}</p>
|
|
<p style="color:#64748b;margin:0;line-height:1.6;font-size:13px">Log in to ShieldAI for more details.</p>`,
|
|
),
|
|
text: brandedText(
|
|
`[${severity.toUpperCase()}] Alert: ${alertTitle}\n\n${alertMessage}\n\nLog in to ShieldAI for more details.`,
|
|
),
|
|
};
|
|
}
|
|
|
|
export function passwordResetEmail(resetLink: string): EmailTemplate {
|
|
return {
|
|
subject: "Reset your ShieldAI password",
|
|
html: brandedWrapper(
|
|
"Password Reset",
|
|
`<p style="color:#334155;margin:0 0 12px;line-height:1.6">You requested a password reset. Click the button below to set a new password.</p>
|
|
<table cellpadding="0" cellspacing="0" style="margin:24px 0">
|
|
<tr>
|
|
<td align="center">
|
|
<a href="${resetLink}" style="display:inline-block;padding:12px 24px;background-color:#1a1a2e;color:#ffffff;text-decoration:none;border-radius:6px;font-size:14px;font-weight:600">Reset Password</a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p style="color:#64748b;margin:0;line-height:1.6;font-size:13px">If you didn't request this, you can safely ignore this email. The link expires in 1 hour.</p>`,
|
|
),
|
|
text: brandedText(
|
|
`Password Reset\n\nYou requested a password reset. Use this link to set a new password:\n${resetLink}\n\nIf you didn't request this, you can safely ignore this email. The link expires in 1 hour.`,
|
|
),
|
|
};
|
|
}
|
|
|
|
export function familyInviteEmail(
|
|
inviterName: string,
|
|
groupName: string,
|
|
acceptLink: string,
|
|
): EmailTemplate {
|
|
return {
|
|
subject: `${inviterName} invited you to ${groupName} on ShieldAI`,
|
|
html: brandedWrapper(
|
|
"Family Invitation",
|
|
`<p style="color:#334155;margin:0 0 12px;line-height:1.6"><strong>${inviterName}</strong> has invited you to join <strong>${groupName}</strong> on ShieldAI.</p>
|
|
<p style="color:#334155;margin:0 0 12px;line-height:1.6">As a family member, you'll get shared protection and alerts for your digital identity.</p>
|
|
<table cellpadding="0" cellspacing="0" style="margin:24px 0">
|
|
<tr>
|
|
<td align="center">
|
|
<a href="${acceptLink}" style="display:inline-block;padding:12px 24px;background-color:#1a1a2e;color:#ffffff;text-decoration:none;border-radius:6px;font-size:14px;font-weight:600">Accept Invitation</a>
|
|
</td>
|
|
</tr>
|
|
</table>`,
|
|
),
|
|
text: brandedText(
|
|
`Family Invitation\n\n${inviterName} has invited you to join ${groupName} on ShieldAI.\n\nAs a family member, you'll get shared protection and alerts for your digital identity.\n\nAccept the invitation: ${acceptLink}`,
|
|
),
|
|
};
|
|
}
|
|
|
|
export function billingReceiptEmail(
|
|
planName: string,
|
|
amount: string,
|
|
date: string,
|
|
receiptUrl: string,
|
|
): EmailTemplate {
|
|
return {
|
|
subject: `ShieldAI receipt — ${planName} (${date})`,
|
|
html: brandedWrapper(
|
|
"Payment Receipt",
|
|
`<p style="color:#334155;margin:0 0 8px;line-height:1.6">Thank you for your payment.</p>
|
|
<table width="100%" cellpadding="8" cellspacing="0" style="margin:16px 0;background-color:#f8fafc;border-radius:6px">
|
|
<tr>
|
|
<td style="color:#64748b;font-size:13px">Plan</td>
|
|
<td style="color:#1a1a2e;font-weight:600;text-align:right">${planName}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="color:#64748b;font-size:13px">Amount</td>
|
|
<td style="color:#1a1a2e;font-weight:600;text-align:right">${amount}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="color:#64748b;font-size:13px">Date</td>
|
|
<td style="color:#1a1a2e;font-weight:600;text-align:right">${date}</td>
|
|
</tr>
|
|
</table>
|
|
<table cellpadding="0" cellspacing="0" style="margin:24px 0">
|
|
<tr>
|
|
<td align="center">
|
|
<a href="${receiptUrl}" style="display:inline-block;padding:10px 20px;background-color:#1a1a2e;color:#ffffff;text-decoration:none;border-radius:6px;font-size:13px;font-weight:600">View Receipt</a>
|
|
</td>
|
|
</tr>
|
|
</table>`,
|
|
),
|
|
text: brandedText(
|
|
`Payment Receipt\n\nThank you for your payment.\n\nPlan: ${planName}\nAmount: ${amount}\nDate: ${date}\n\nView receipt: ${receiptUrl}`,
|
|
),
|
|
};
|
|
}
|