Files
QR-master/src/lib/email.ts

1438 lines
70 KiB
TypeScript

/**
* Email Templates - CRO-Optimized
*
* What's improved (based on email marketing best practices):
*
* Subject Lines:
* - More specific with urgency/outcome ("Expires in 1 Hour")
* - Emoji for visual attention in inbox
* - Clear value proposition
*
* Design & Copy:
* - Personal sender name ("Timo from QR Master")
* - Storytelling instead of feature lists
* - Trust elements (security badges, timelines)
* - Clear CTAs with action verbs ("Reset My Password →")
* - Objection handling built-in ("Didn't request this?")
* - Mobile-optimized (shorter paragraphs, more whitespace)
*
* How to measure if this works for YOUR audience:
* 1. Track baseline metrics (open rate, click rate, conversions)
* 2. A/B test old vs new templates
* 3. Measure with real users (minimum 100 emails per variant)
*
* No guarantees - only your data will tell what works for QR Master users.
*/
import { Resend } from 'resend';
import nodemailer from 'nodemailer';
// Use a placeholder during build time, real key at runtime
const resendKey = process.env.RESEND_API_KEY || 're_placeholder_for_build';
const resend = new Resend(resendKey);
// Rate limiter for Resend Free Tier (1 email per second)
let lastEmailSent = 0;
const MIN_EMAIL_INTERVAL = 1000; // 1 second in milliseconds
async function waitForRateLimit() {
const now = Date.now();
const timeSinceLastEmail = now - lastEmailSent;
if (timeSinceLastEmail < MIN_EMAIL_INTERVAL) {
const waitTime = MIN_EMAIL_INTERVAL - timeSinceLastEmail;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
lastEmailSent = Date.now();
}
/**
* Password Reset Email - Security focused with clear urgency
*/
export async function sendPasswordResetEmail(email: string, resetToken: string) {
await waitForRateLimit();
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
const resetUrl = `${appUrl}/reset-password?token=${resetToken}`;
try {
await resend.emails.send({
from: 'QR Master Security <noreply@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: '🔐 Reset Your QR Master Password (Expires in 1 Hour)',
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset Your Password - QR Master</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f4;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f4; padding: 40px 20px;">
<tr>
<td align="center">
<!-- Main Container -->
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.08);">
<!-- Header with Security Badge -->
<tr>
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 50px 40px; text-align: center;">
<div style="display: inline-block; background-color: rgba(255,255,255,0.2); padding: 12px 24px; border-radius: 50px; margin-bottom: 20px;">
<span style="color: #ffffff; font-size: 14px; font-weight: 600; letter-spacing: 1px;">🔒 SECURITY ALERT</span>
</div>
<h1 style="margin: 0; color: #ffffff; font-size: 32px; font-weight: bold; line-height: 1.2;">Password Reset Request</h1>
<p style="margin: 15px 0 0 0; color: rgba(255,255,255,0.9); font-size: 16px;">Someone requested to reset your password</p>
</td>
</tr>
<!-- Content Section -->
<tr>
<td style="padding: 50px 40px;">
<p style="margin: 0 0 20px 0; color: #333333; font-size: 16px; line-height: 1.6;">
Hi there,
</p>
<p style="margin: 0 0 25px 0; color: #555555; font-size: 16px; line-height: 1.7;">
We received a request to reset the password for your QR Master account. If this was you, click the button below to create a new password:
</p>
<!-- Primary CTA Button -->
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 35px 0;">
<tr>
<td align="center">
<a href="${resetUrl}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 18px 50px; border-radius: 8px; font-size: 17px; font-weight: 700; display: inline-block; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);">
🔐 Reset My Password →
</a>
</td>
</tr>
</table>
<!-- Security Info Box -->
<div style="background: linear-gradient(135deg, rgba(255, 107, 107, 0.08) 0%, rgba(255, 148, 148, 0.08) 100%); border-left: 4px solid #ff6b6b; padding: 20px 25px; margin: 35px 0; border-radius: 6px;">
<p style="margin: 0 0 12px 0; color: #d63031; font-size: 15px; font-weight: 700;">
⏱️ This link expires in 60 minutes
</p>
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6;">
For your security, this password reset link will only work once and expires after 1 hour. If you need a new link, you can request another one anytime.
</p>
</div>
<!-- Alternative Link -->
<p style="margin: 30px 0 15px 0; color: #888888; font-size: 14px; line-height: 1.5;">
🔗 Button not working? Copy and paste this link into your browser:
</p>
<p style="margin: 0 0 30px 0; padding: 18px 20px; background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; word-break: break-all;">
<a href="${resetUrl}" style="color: #667eea; text-decoration: none; font-size: 13px; font-family: monospace;">${resetUrl}</a>
</p>
<!-- Didn't Request This? -->
<div style="background-color: #f8f9fa; padding: 25px; border-radius: 8px; margin-top: 35px;">
<p style="margin: 0 0 12px 0; color: #333333; font-size: 15px; font-weight: 600;">
❓ Didn't request this?
</p>
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6;">
✅ You can safely ignore this email. Your password won't be changed unless you click the button above. If you're concerned about your account security, please contact us immediately at <a href="mailto:support@qrmaster.net" style="color: #667eea; text-decoration: none;">support@qrmaster.net</a>
</p>
</div>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8f9fa; padding: 35px 40px; border-top: 1px solid #e9ecef;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<p style="margin: 0 0 15px 0; color: #333333; font-size: 16px; font-weight: 600;">
QR Master
</p>
<p style="margin: 0 0 8px 0; color: #888888; font-size: 13px;">
Secure QR Code Analytics & Management
</p>
<p style="margin: 0; color: #999999; font-size: 12px;">
© 2026 QR Master. All rights reserved.
</p>
<p style="margin: 15px 0 0 0; color: #aaaaaa; font-size: 11px;">
This is an automated security email. Please do not reply.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`,
});
console.log('Password reset email sent successfully to:', email);
return { success: true };
} catch (error) {
console.error('Error sending password reset email:', error);
throw new Error('Failed to send password reset email');
}
}
/**
* Newsletter Welcome Email - Value-focused with clear expectations
*/
export async function sendNewsletterWelcomeEmail(email: string) {
await waitForRateLimit();
try {
await resend.emails.send({
from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: '🎉 You\'re In! Here\'s What Happens Next (AI QR Features)',
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to QR Master AI Waitlist</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f4;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f4; padding: 40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.08);">
<!-- Hero Header -->
<tr>
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 50px 40px; text-align: center;">
<div style="display: inline-block; background-color: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 50px; margin-bottom: 20px;">
<span style="color: #ffffff; font-size: 13px; font-weight: 600; letter-spacing: 1px;">✨ EARLY ACCESS</span>
</div>
<h1 style="margin: 0; color: #ffffff; font-size: 36px; font-weight: bold; line-height: 1.2;">You're on the List! 🚀</h1>
<p style="margin: 20px 0 0 0; color: rgba(255,255,255,0.95); font-size: 18px; line-height: 1.5;">Get ready for AI-powered QR codes that work smarter, not harder</p>
</td>
</tr>
<!-- Personal Note -->
<tr>
<td style="padding: 45px 40px 35px 40px;">
<p style="margin: 0 0 20px 0; color: #333333; font-size: 16px; line-height: 1.7;">
Hey there! 👋
</p>
<p style="margin: 0 0 25px 0; color: #555555; font-size: 16px; line-height: 1.7;">
Thanks for joining the waitlist for QR Master's AI-powered features. You're among the <strong>first to know</strong> when we launch something that'll completely change how you create and manage QR codes.
</p>
<p style="margin: 0 0 35px 0; color: #555555; font-size: 16px; line-height: 1.7;">
No fluff, no spam. Just a heads-up when these features go live.
</p>
<!-- What's Coming Section -->
<div style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%); border-left: 4px solid #667eea; padding: 30px 30px; margin: 35px 0; border-radius: 8px;">
<h2 style="margin: 0 0 20px 0; color: #667eea; font-size: 20px; font-weight: 700;">
🎯 What You Can Expect
</h2>
<!-- Feature 1 -->
<div style="margin-bottom: 20px;">
<h3 style="margin: 0 0 8px 0; color: #333333; font-size: 16px; font-weight: 700;">
⚡ Smart QR Generation
</h3>
<p style="margin: 0; color: #666666; font-size: 15px; line-height: 1.6;">
AI analyzes your content and automatically suggests the optimal QR design, colors, and format for maximum scans. No more guessing what works.
</p>
</div>
<!-- Feature 2 -->
<div style="margin-bottom: 20px;">
<h3 style="margin: 0 0 8px 0; color: #333333; font-size: 16px; font-weight: 700;">
📊 Predictive Analytics
</h3>
<p style="margin: 0; color: #666666; font-size: 15px; line-height: 1.6;">
See scan forecasts, detect unusual patterns, and ask questions about your data in plain English. Your analytics become conversations, not spreadsheets.
</p>
</div>
<!-- Feature 3 -->
<div style="margin-bottom: 20px;">
<h3 style="margin: 0 0 8px 0; color: #333333; font-size: 16px; font-weight: 700;">
🤖 Auto-Organization
</h3>
<p style="margin: 0; color: #666666; font-size: 15px; line-height: 1.6;">
Your QR codes automatically get categorized, tagged, and organized. Spend less time managing, more time creating.
</p>
</div>
<!-- Feature 4 -->
<div>
<h3 style="margin: 0 0 8px 0; color: #333333; font-size: 16px; font-weight: 700;">
🎨 AI Design Studio
</h3>
<p style="margin: 0; color: #666666; font-size: 15px; line-height: 1.6;">
Generate unique, on-brand QR code designs in seconds. Just describe what you want, AI handles the rest.
</p>
</div>
</div>
<!-- Timeline -->
<div style="background-color: #fff3cd; border: 1px solid #ffc107; padding: 25px; border-radius: 8px; margin: 35px 0;">
<p style="margin: 0 0 12px 0; color: #856404; font-size: 15px; font-weight: 700;">
⏰ When Will This Happen?
</p>
<p style="margin: 0; color: #856404; font-size: 14px; line-height: 1.6;">
We're in active development and testing. You'll get an email the moment these features go live - <strong>no waiting, no wondering</strong>. We respect your inbox.
</p>
</div>
<!-- Meanwhile CTA -->
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px; margin: 35px 0; text-align: center;">
<h3 style="margin: 0 0 15px 0; color: #333333; font-size: 18px; font-weight: 700;">
💡 Want to Get Started Now?
</h3>
<p style="margin: 0 0 25px 0; color: #666666; font-size: 15px; line-height: 1.6;">
Our current platform already helps teams create, track, and manage dynamic QR codes. No AI needed - just powerful tools that work today.
</p>
<a href="https://www.qrmaster.net/signup" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 16px 40px; border-radius: 8px; font-size: 16px; font-weight: 700; display: inline-block; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);">
🚀 Try QR Master Free →
</a>
</div>
<!-- Personal Sign-off -->
<p style="margin: 35px 0 0 0; color: #555555; font-size: 16px; line-height: 1.7;">
💌 Thanks for trusting us with your inbox. We'll make it worth it.
</p>
<p style="margin: 20px 0 0 0; color: #555555; font-size: 16px; line-height: 1.7;">
<strong>- Timo 👋</strong><br>
<span style="color: #888888; font-size: 14px;">Founder, QR Master</span>
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8f9fa; padding: 35px 40px; border-top: 1px solid #e9ecef;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<p style="margin: 0 0 8px 0; color: #888888; font-size: 13px;">
<a href="https://www.qrmaster.net" style="color: #667eea; text-decoration: none;">www.qrmaster.net</a>
</p>
<p style="margin: 0; color: #999999; font-size: 12px;">
© 2026 QR Master. All rights reserved.
</p>
<p style="margin: 15px 0 0 0; color: #aaaaaa; font-size: 11px;">
You're receiving this because you signed up for AI feature notifications.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`,
});
console.log('Newsletter welcome email sent successfully to:', email);
return { success: true };
} catch (error) {
console.error('Error sending newsletter welcome email:', error);
throw new Error('Failed to send newsletter welcome email');
}
}
/**
* AI Feature Launch Email - Excitement + clear CTA
*/
export async function sendAIFeatureLaunchEmail(email: string) {
await waitForRateLimit();
try {
await resend.emails.send({
from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: '🚀 They\'re Live! Your AI QR Features Are Ready',
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Features Are Live - QR Master</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f4;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f4; padding: 40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.08);">
<!-- Celebration Header -->
<tr>
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 25px 30px; text-align: center;">
<div style="font-size: 32px; margin-bottom: 10px;">🎉</div>
<div style="display: inline-block; background-color: rgba(255,255,255,0.2); padding: 6px 14px; border-radius: 50px; margin-bottom: 12px;">
<span style="color: #ffffff; font-size: 11px; font-weight: 600; letter-spacing: 1px;">✨ NOW LIVE</span>
</div>
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: bold; line-height: 1.2;">AI Features Are Here! 🚀</h1>
<p style="margin: 10px 0 0 0; color: rgba(255,255,255,0.95); font-size: 14px; line-height: 1.4;">Ready to use in your dashboard</p>
</td>
</tr>
<!-- Main Content -->
<tr>
<td style="padding: 50px 40px;">
<!-- Opening -->
<p style="margin: 0 0 25px 0; color: #333333; font-size: 17px; line-height: 1.7;">
Remember when you signed up to be notified about our AI features?
</p>
<p style="margin: 0 0 25px 0; color: #333333; font-size: 17px; line-height: 1.7; font-weight: 700;">
Well, today's that day. 🎯
</p>
<p style="margin: 0 0 35px 0; color: #555555; font-size: 16px; line-height: 1.7;">
We've been building, testing, and polishing these features for months. And they're finally ready for you to use. Right now. No beta, no waitlist, no BS.
</p>
<!-- What's New Highlight Box -->
<div style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 2px solid #667eea; padding: 35px 30px; margin: 40px 0; border-radius: 10px;">
<h2 style="margin: 0 0 25px 0; color: #667eea; font-size: 22px; font-weight: 700; text-align: center;">
✨ What's New in Your Dashboard
</h2>
<!-- Feature Cards -->
<div style="background-color: #ffffff; padding: 25px; margin-bottom: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
<h3 style="margin: 0 0 10px 0; color: #333333; font-size: 17px; font-weight: 700;">
⚡ Smart QR Generation
</h3>
<p style="margin: 0; color: #666666; font-size: 15px; line-height: 1.6;">
AI analyzes your content and suggests optimal designs automatically. Just paste your link, we handle the optimization.
</p>
</div>
<div style="background-color: #ffffff; padding: 25px; margin-bottom: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
<h3 style="margin: 0 0 10px 0; color: #333333; font-size: 17px; font-weight: 700;">
📊 Predictive Analytics
</h3>
<p style="margin: 0; color: #666666; font-size: 15px; line-height: 1.6;">
Ask questions about your scan data in plain English. "Which campaign performed best last month?" Done.
</p>
</div>
<div style="background-color: #ffffff; padding: 25px; margin-bottom: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
<h3 style="margin: 0 0 10px 0; color: #333333; font-size: 17px; font-weight: 700;">
🤖 Auto-Organization
</h3>
<p style="margin: 0; color: #666666; font-size: 15px; line-height: 1.6;">
QR codes automatically categorized and tagged. Your library stays organized without manual work.
</p>
</div>
<div style="background-color: #ffffff; padding: 25px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
<h3 style="margin: 0 0 10px 0; color: #333333; font-size: 17px; font-weight: 700;">
🎨 AI Design Studio
</h3>
<p style="margin: 0; color: #666666; font-size: 15px; line-height: 1.6;">
Generate custom QR designs by describing what you want. "Make it modern and blue" → Done in 3 seconds.
</p>
</div>
</div>
<!-- How to Access -->
<div style="background-color: #fff3cd; border: 2px solid #ffc107; padding: 30px; border-radius: 10px; margin: 40px 0;">
<h3 style="margin: 0 0 15px 0; color: #856404; font-size: 18px; font-weight: 700;">
🎯 How to Get Started
</h3>
<ol style="margin: 0; padding-left: 20px; color: #856404; font-size: 15px; line-height: 1.8;">
<li style="margin-bottom: 10px;">🔐 Log in to your QR Master account</li>
<li style="margin-bottom: 10px;">👀 Look for the "✨ AI" badge on supported features</li>
<li style="margin-bottom: 10px;">✏️ Try creating a QR code - you'll see AI suggestions automatically</li>
<li>📈 Check your Analytics tab for the new AI query interface</li>
</ol>
</div>
<!-- Primary CTA -->
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 45px 0 40px 0;">
<tr>
<td align="center">
<a href="https://www.qrmaster.net/create" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 20px 60px; border-radius: 10px; font-size: 18px; font-weight: 700; display: inline-block; box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);">
✨ Try AI Features Now →
</a>
<p style="margin: 20px 0 0 0; color: #888888; font-size: 13px;">
✅ Available on all plans • ⚡ No extra setup required
</p>
</td>
</tr>
</table>
<!-- Personal Note -->
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px; margin: 40px 0;">
<p style="margin: 0 0 20px 0; color: #555555; font-size: 15px; line-height: 1.7;">
💬 We're excited to see what you build with these new tools. If you have questions, ideas, or just want to share what you created - <a href="mailto:support@qrmaster.net" style="color: #667eea; text-decoration: none;">hit reply</a>. I read every email.
</p>
<p style="margin: 0; color: #555555; font-size: 15px; line-height: 1.7;">
<strong>- Timo 👋</strong><br>
<span style="color: #888888; font-size: 13px;">Founder, QR Master</span>
</p>
</div>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8f9fa; padding: 35px 40px; border-top: 1px solid #e9ecef;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<p style="margin: 0 0 8px 0; color: #888888; font-size: 13px;">
<a href="https://www.qrmaster.net" style="color: #667eea; text-decoration: none;">www.qrmaster.net</a> •
<a href="https://www.qrmaster.net/dashboard" style="color: #667eea; text-decoration: none;">Dashboard</a> •
<a href="https://www.qrmaster.net/faq" style="color: #667eea; text-decoration: none;">Help</a>
</p>
<p style="margin: 0; color: #999999; font-size: 12px;">
© 2026 QR Master. All rights reserved.
</p>
<p style="margin: 15px 0 0 0; color: #aaaaaa; font-size: 11px;">
You received this because you subscribed to AI feature launch notifications.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`,
});
console.log('AI feature launch email sent successfully to:', email);
return { success: true };
} catch (error) {
console.error('Error sending AI feature launch email:', error);
throw new Error('Failed to send AI feature launch email');
}
}
// ---------------------------------------------------------------------------
// SMTP Transport (nodemailer) - used for retention / welcome emails
// ---------------------------------------------------------------------------
function createSmtpTransport() {
const host = process.env.SMTP_HOST || 'smtp.qrmaster.net';
const port = parseInt(process.env.SMTP_PORT || '465', 10);
const user = process.env.SMTP_USER || 'timo@qrmaster.net';
const pass = process.env.SMTP_PASS?.trim();
if (!pass) {
throw new Error('SMTP_PASS is missing. Configure SMTP credentials before sending welcome or retention emails.');
}
return nodemailer.createTransport({
host,
port,
secure: port === 465,
auth: {
user,
pass,
},
});
}
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://www.qrmaster.net';
// ---------------------------------------------------------------------------
// Shared design tokens (email-safe inline styles)
// ---------------------------------------------------------------------------
const clr = {
bg: '#F5F2EC', // warm parchment wrapper
card: '#FFFFFF',
header: '#0B0D14', // near-black header
headerAccent: '#1A1D2E',
gold: '#C8A257', // warm gold accent
goldDim: '#A07E3A',
text: '#1A1A1A',
textSoft: '#5A5A5A',
textMuted: '#909090',
border: '#E8E3D8',
pillBg: '#F0EDE5',
ctaBg: '#0B0D14',
ctaText: '#FFFFFF',
};
const webFontHead = `
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:wght@300;400;500;600&display=swap');
.dm-serif { font-family: 'DM Serif Display', Georgia, 'Times New Roman', serif !important; }
.dm-sans { font-family: 'DM Sans', -apple-system, 'Helvetica Neue', Arial, sans-serif !important; }
</style>`;
function emailShell(headExtra: string, bodyContent: string): string {
return `<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
${webFontHead}
${headExtra}
</head>
<body style="margin:0;padding:0;background-color:${clr.bg};-webkit-text-size-adjust:100%;mso-line-height-rule:exactly;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"
style="background-color:${clr.bg};padding:40px 16px 60px;">
<tr><td align="center">
<!-- Email card -->
<table role="presentation" width="560" cellpadding="0" cellspacing="0" border="0"
style="max-width:560px;width:100%;background-color:${clr.card};border-radius:16px;
overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,0.12);">
${bodyContent}
</table>
<!-- Footer text -->
<table role="presentation" width="560" cellpadding="0" cellspacing="0" border="0"
style="max-width:560px;width:100%;margin-top:28px;">
<tr>
<td style="text-align:center;padding:0 20px;">
<p style="margin:0 0 6px;font-family:'DM Sans',-apple-system,sans-serif;font-size:12px;color:${clr.textMuted};">
<a href="${appUrl}" style="color:${clr.gold};text-decoration:none;font-weight:500;">www.qrmaster.net</a>
&nbsp;·&nbsp;
<a href="mailto:support@qrmaster.net" style="color:${clr.textMuted};text-decoration:none;">support@qrmaster.net</a>
</p>
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;font-size:11px;color:#B0A898;">
© 2026 QR Master. You're receiving this because you created an account.
</p>
</td>
</tr>
</table>
</td></tr>
</table>
</body>
</html>`;
}
// Dot-grid SVG encoded as data URI for header backgrounds
// Safe URL-encoded SVG without problematic internal quotes
const dotGridPattern = `url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%221.5%22%20fill%3D%22%23C8A257%22%20fill-opacity%3D%220.18%22%2F%3E%3C%2Fsvg%3E)`;
/**
* Welcome Email - sent immediately on signup (Day 0)
*/
export async function sendWelcomeEmail(email: string, name: string) {
const transport = createSmtpTransport();
const createUrl = `${appUrl}/create`;
const firstName = name.split(' ')[0];
const html = emailShell('', `
<!-- ── HEADER ── -->
<tr>
<td style="background-color:${clr.card};background-image:${dotGridPattern};
background-size:24px 24px;padding:60px 48px 50px;text-align:center;">
<!-- Wordmark Badge -->
<table role="presentation" cellpadding="0" cellspacing="0" border="0" align="center">
<tr>
<td style="border:1px solid rgba(200,162,87,0.4);border-radius:6px;
padding:6px 16px;background-color:rgba(200,162,87,0.05);">
<span style="font-family:'DM Sans',-apple-system,sans-serif;font-size:10px;
font-weight:700;letter-spacing:3px;color:${clr.gold};
text-transform:uppercase;">QR Master</span>
</td>
</tr>
</table>
<!-- Headline -->
<h1 style="margin:36px 0 0;font-family:'DM Serif Display',Georgia,serif;
font-size:42px;font-weight:400;line-height:1.1;color:${clr.text};
letter-spacing:-0.5px;">
Welcome,<br>
<em style="color:${clr.gold};font-style:italic;">${firstName}.</em>
</h1>
<!-- Thin gold divider -->
<table role="presentation" cellpadding="0" cellspacing="0" border="0"
align="center" style="margin-top:32px;">
<tr>
<td style="width:40px;height:1px;background-color:${clr.gold};opacity:0.6;"></td>
<td style="width:12px;"></td>
<td style="width:6px;height:6px;background-color:${clr.gold};border-radius:50%;
opacity:0.9;"></td>
<td style="width:12px;"></td>
<td style="width:40px;height:1px;background-color:${clr.gold};opacity:0.6;"></td>
</tr>
</table>
<p style="margin:24px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:15px;color:${clr.textSoft};letter-spacing:0.3px;">
The physical world is now your canvas.
</p>
</td>
</tr>
<!-- ── HERO IMAGE ── -->
<tr>
<td style="padding: 0; text-align: center; background-color: ${clr.card};">
<img src="${appUrl}/email-hero-light.png" width="560" style="display:block;width:100%;max-width:560px;height:auto;border-bottom:3px solid ${clr.gold};" alt="Beautiful QR Code Experience">
</td>
</tr>
<!-- ── BODY ── -->
<tr>
<td style="padding:56px 48px 0;">
<p style="margin:0 0 24px;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.8;color:${clr.text};">
Let's be honest: most QR codes are static, look terrible, and break the moment you change a link. We built QR Master to fix that.
</p>
<p style="margin:0 0 24px;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.8;color:${clr.text};">
Your account is fully activated. You now have the power to create beautiful, dynamic QR codes that adapt to your brand, never expire, and track every single scan (device, location, and time).
</p>
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.8;color:${clr.textSoft};">
To give you the perfect start, we've loaded your account with everything you need:
</p>
</td>
</tr>
<!-- ── STATS STRIP ── -->
<tr>
<td style="padding:16px 48px 32px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<!-- Stat 1 -->
<td style="padding:12px 0;text-align:center;width:31%;">
<div style="font-family:'DM Serif Display',Georgia,serif;font-size:28px;
color:${clr.text};line-height:1;">3</div>
<div style="font-family:'DM Sans',-apple-system,sans-serif;font-size:10px;
font-weight:700;color:${clr.textMuted};letter-spacing:1px;margin-top:8px;
text-transform:uppercase;">
Free Codes
</div>
</td>
<td style="width:3.5%;"></td>
<!-- Stat 2 -->
<td style="padding:12px 0;text-align:center;width:31%;">
<div style="font-family:'DM Serif Display',Georgia,serif;font-size:28px;
color:${clr.text};line-height:1;">90<span style="font-size:18px;">s</span></div>
<div style="font-family:'DM Sans',-apple-system,sans-serif;font-size:10px;
font-weight:700;color:${clr.textMuted};letter-spacing:1px;margin-top:8px;
text-transform:uppercase;">
To Create
</div>
</td>
<td style="width:3.5%;"></td>
<!-- Stat 3 -->
<td style="padding:12px 0;text-align:center;width:31%;">
<div style="font-family:'DM Sans',-apple-system,sans-serif;font-size:28px;
color:${clr.text};line-height:1;">∞</div>
<div style="font-family:'DM Sans',-apple-system,sans-serif;font-size:10px;
font-weight:700;color:${clr.textMuted};letter-spacing:1px;margin-top:8px;
text-transform:uppercase;">
No Expiry
</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- ── CTA ── -->
<tr>
<td style="padding:12px 48px 56px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td align="center">
<a href="${createUrl}"
style="display:inline-block;background-color:${clr.ctaBg};color:${clr.ctaText};
text-decoration:none;padding:18px 48px;border-radius:8px;
font-family:'DM Sans',-apple-system,sans-serif;font-size:16px;
font-weight:600;letter-spacing:0.5px;
border:1px solid rgba(200,162,87,0.5);
box-shadow:0 6px 20px rgba(11,13,20,0.15);">
Design your first QR code &nbsp;→
</a>
</td>
</tr>
<tr>
<td align="center" style="padding-top:16px;">
<span style="font-family:'DM Sans',-apple-system,sans-serif;font-size:13px;
color:${clr.textMuted};">No credit card required. Zero commitment.</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- ── DIVIDER ── -->
<tr>
<td style="padding:0 48px;">
<div style="border-top:1px solid ${clr.border};"></div>
</td>
</tr>
<!-- ── SIGN-OFF ── -->
<tr>
<td style="padding:40px 48px 56px;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:15px;color:${clr.textSoft};line-height:1.7;">
I'm thrilled to have you on board. If you have any questions, feedback, or just want to share what you've created - hit reply. I'm reading every single email.
</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin-top:28px;">
<tr>
<td style="padding-right:16px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="width:56px; height:56px; background-color:#0B0D14; border-radius:50%; text-align:center; vertical-align:middle; border:2px solid ${clr.border}; box-shadow:0 4px 10px rgba(0,0,0,0.05);">
<img src="${appUrl}/favicon1.png" width="32" height="32" alt="Timo" style="display:inline-block; vertical-align:middle; border-radius:50%; object-fit:cover;">
</td>
</tr>
</table>
</td>
<td>
<p style="margin:0;font-family:'DM Serif Display',Georgia,serif;
font-size:24px;font-style:italic;color:${clr.text};line-height:1.2;">
Timo
</p>
<p style="margin:4px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:11px;font-weight:700;color:${clr.textMuted};
letter-spacing:1.5px;text-transform:uppercase;">
Founder, QR Master
</p>
</td>
</tr>
</table>
</td>
</tr>
`);
await transport.sendMail({
from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: 'Your QR Master account is ready',
html,
});
}
/**
* Activation Nudge - sent on Day 3 if user has 0 QR codes
*/
export async function sendActivationNudgeEmail(email: string, name: string) {
const transport = createSmtpTransport();
const createUrl = `${appUrl}/create`;
const firstName = name.split(' ')[0];
const steps = [
{ n: '01', label: 'Paste your URL', sub: 'or choose WiFi, vCard, Teams, and more' },
{ n: '02', label: 'Customize the design', sub: 'colors, logo, frame label - optional' },
{ n: '03', label: 'Download & use', sub: 'PNG for screen, SVG for print' },
];
const stepsHtml = steps.map(s => `
<tr>
<td style="padding:16px 0;border-bottom:1px solid ${clr.border};">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="width:36px;vertical-align:top;padding-top:2px;">
<span style="font-family:'DM Serif Display',Georgia,serif;font-size:13px;
color:${clr.gold};font-style:italic;">${s.n}</span>
</td>
<td>
<div style="font-family:'DM Sans',-apple-system,sans-serif;font-size:15px;
font-weight:600;color:${clr.text};">${s.label}</div>
<div style="font-family:'DM Sans',-apple-system,sans-serif;font-size:13px;
color:${clr.textMuted};margin-top:2px;">${s.sub}</div>
</td>
</tr>
</table>
</td>
</tr>`).join('');
const html = emailShell('', `
<!-- ── HEADER ── -->
<tr>
<td style="background-color:${clr.header};background-image:${dotGridPattern};
background-size:24px 24px;padding:44px 48px 40px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="border:1px solid rgba(200,162,87,0.35);border-radius:8px;padding:7px 18px;">
<span style="font-family:'DM Sans',-apple-system,sans-serif;font-size:11px;
font-weight:600;letter-spacing:3px;color:${clr.gold};
text-transform:uppercase;">QR Master</span>
</td>
</tr>
</table>
<h1 style="margin:28px 0 0;font-family:'DM Serif Display',Georgia,serif;
font-size:34px;font-weight:400;line-height:1.2;color:#FFFFFF;">
You haven't made one yet,<br>
<em style="color:${clr.gold};">${firstName}.</em>
</h1>
<p style="margin:16px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;color:rgba(255,255,255,0.45);letter-spacing:0.2px;">
3 days since signup · 0 QR codes created
</p>
</td>
</tr>
<!-- ── BODY ── -->
<tr>
<td style="padding:40px 48px 0;">
<p style="margin:0 0 8px;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.text};">
Your 3 free dynamic QR codes are still there. Unused.<br>
Dynamic means: one code, update the link anytime, every scan tracked.
</p>
<p style="margin:16px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:15px;line-height:1.75;color:${clr.textSoft};">
Here's all it takes:
</p>
</td>
</tr>
<!-- ── STEPS ── -->
<tr>
<td style="padding:8px 48px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
${stepsHtml}
</table>
</td>
</tr>
<!-- ── CTA ── -->
<tr>
<td style="padding:36px 48px 44px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td align="center">
<a href="${createUrl}"
style="display:inline-block;background-color:${clr.ctaBg};color:${clr.ctaText};
text-decoration:none;padding:18px 52px;border-radius:10px;
font-family:'DM Sans',-apple-system,sans-serif;font-size:15px;
font-weight:600;letter-spacing:0.3px;border:1px solid rgba(200,162,87,0.3);">
Make my first QR code &nbsp;→
</a>
</td>
</tr>
<tr>
<td align="center" style="padding-top:12px;">
<span style="font-family:'DM Sans',-apple-system,sans-serif;font-size:12px;
color:${clr.textMuted};">Free forever · no card needed</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- ── DIVIDER + SIGN-OFF ── -->
<tr><td style="padding:0 48px;"><div style="border-top:1px solid ${clr.border};"></div></td></tr>
<tr>
<td style="padding:28px 48px 40px;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;color:${clr.textSoft};line-height:1.7;">
If something stopped you from getting started, just reply. I'll help directly.
</p>
<p style="margin:16px 0 0;font-family:'DM Serif Display',Georgia,serif;
font-size:17px;color:${clr.text};">Timo</p>
<p style="margin:2px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:11px;color:${clr.textMuted};letter-spacing:0.5px;">FOUNDER, QR MASTER</p>
</td>
</tr>
`);
await transport.sendMail({
from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: "Your 3 free codes are still sitting there",
html,
});
}
/**
* Upgrade Nudge - sent on Day 7 if user has ≥1 QR code and is still on FREE plan
*/
export async function sendUpgradeNudgeEmail(email: string, name: string, qrCount: number) {
const transport = createSmtpTransport();
const pricingUrl = `${appUrl}/pricing`;
const firstName = name.split(' ')[0];
const features = [
{
label: 'Dynamic QR codes',
free: '3',
pro: '50',
},
{
label: 'Logo in QR code',
free: '-',
pro: 'Your logo, centered',
},
{
label: 'Module shapes',
free: '-',
pro: '4 shapes + eye styles',
},
{
label: 'Scan detail',
free: 'Total scans',
pro: 'Device, location, time',
},
];
const rowsHtml = features.map((f, i) => `
<tr style="background-color:${i % 2 === 0 ? '#FAFAF8' : '#FFFFFF'};">
<td style="padding:12px 16px;font-family:'DM Sans',-apple-system,sans-serif;
font-size:13px;color:${clr.textSoft};border-bottom:1px solid ${clr.border};">
${f.label}
</td>
<td style="padding:12px 16px;text-align:center;font-family:'DM Sans',-apple-system,sans-serif;
font-size:13px;color:${clr.textMuted};border-bottom:1px solid ${clr.border};">
${f.free}
</td>
<td style="padding:12px 16px;text-align:center;font-family:'DM Sans',-apple-system,sans-serif;
font-size:13px;font-weight:500;color:${clr.text};border-bottom:1px solid ${clr.border};">
${f.pro}
</td>
</tr>`).join('');
const html = emailShell('', `
<!-- ── HEADER ── -->
<tr>
<td style="background-color:${clr.header};background-image:${dotGridPattern};
background-size:24px 24px;padding:44px 48px 40px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="border:1px solid rgba(200,162,87,0.35);border-radius:8px;padding:7px 18px;">
<span style="font-family:'DM Sans',-apple-system,sans-serif;font-size:11px;
font-weight:600;letter-spacing:3px;color:${clr.gold};
text-transform:uppercase;">QR Master</span>
</td>
</tr>
</table>
<h1 style="margin:28px 0 0;font-family:'DM Serif Display',Georgia,serif;
font-size:34px;font-weight:400;line-height:1.2;color:#FFFFFF;">
You are out of slots,<br>
<em style="color:${clr.gold};">${firstName}.</em>
</h1>
<p style="margin:16px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;color:rgba(255,255,255,0.45);">
Free plan · ${qrCount} of 3 dynamic codes active
</p>
</td>
</tr>
<!-- ── BODY ── -->
<tr>
<td style="padding:40px 48px 0;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.text};">
Three codes is enough to prove the idea. It is not enough to run two campaigns at once - from here, every new placement means switching off an old one.
</p>
<p style="margin:16px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.textSoft};">
Pro raises it to 50, and shows you which placement actually earned the scan:
</p>
</td>
</tr>
<!-- ── COMPARISON TABLE ── -->
<tr>
<td style="padding:24px 48px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"
style="border-radius:10px;overflow:hidden;border:1px solid ${clr.border};">
<!-- Table header -->
<tr>
<td style="padding:12px 16px;background-color:${clr.pillBg};
font-family:'DM Sans',-apple-system,sans-serif;font-size:11px;
font-weight:600;letter-spacing:1px;color:${clr.textMuted};
text-transform:uppercase;border-bottom:1px solid ${clr.border};width:45%;">
Feature
</td>
<td style="padding:12px 16px;background-color:${clr.pillBg};text-align:center;
font-family:'DM Sans',-apple-system,sans-serif;font-size:11px;
font-weight:600;letter-spacing:1px;color:${clr.textMuted};
text-transform:uppercase;border-bottom:1px solid ${clr.border};width:20%;">
Free
</td>
<td style="padding:12px 16px;background-color:${clr.header};text-align:center;
font-family:'DM Sans',-apple-system,sans-serif;font-size:11px;
font-weight:600;letter-spacing:1px;color:${clr.gold};
text-transform:uppercase;border-bottom:1px solid ${clr.border};width:35%;">
Pro
</td>
</tr>
${rowsHtml}
</table>
</td>
</tr>
<!-- ── CTA ── -->
<tr>
<td style="padding:32px 48px 44px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td align="center">
<a href="${pricingUrl}"
style="display:inline-block;background-color:${clr.ctaBg};color:${clr.ctaText};
text-decoration:none;padding:18px 52px;border-radius:10px;
font-family:'DM Sans',-apple-system,sans-serif;font-size:15px;
font-weight:600;letter-spacing:0.3px;border:1px solid rgba(200,162,87,0.3);">
See Pro plan &nbsp;→
</a>
</td>
</tr>
<tr>
<td align="center" style="padding-top:12px;">
<span style="font-family:'DM Sans',-apple-system,sans-serif;font-size:12px;
color:${clr.textMuted};">Your free plan stays active - no pressure</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- ── DIVIDER + SIGN-OFF ── -->
<tr><td style="padding:0 48px;"><div style="border-top:1px solid ${clr.border};"></div></td></tr>
<tr>
<td style="padding:28px 48px 40px;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;color:${clr.textSoft};line-height:1.7;">
If you have questions about what Pro unlocks, just reply here.
</p>
<p style="margin:16px 0 0;font-family:'DM Serif Display',Georgia,serif;
font-size:17px;color:${clr.text};">Timo</p>
<p style="margin:2px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:11px;color:${clr.textMuted};letter-spacing:0.5px;">FOUNDER, QR MASTER</p>
</td>
</tr>
`);
await transport.sendMail({
from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: 'You just hit the free limit',
html,
});
}
/**
* 30-Day Nudge - sent on Day 30 if user has ≥1 QR code and is still on FREE plan
*/
export async function sendThirtyDayNudgeEmail(
email: string,
name: string,
qrCount: number,
scanCount: number = 0
) {
const transport = createSmtpTransport();
const pricingUrl = `${appUrl}/pricing`;
const firstName = name.split(' ')[0];
const html = emailShell('', `
<!-- ── HEADER ── -->
<tr>
<td style="background-color:${clr.header};background-image:${dotGridPattern};
background-size:24px 24px;padding:44px 48px 40px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="border:1px solid rgba(200,162,87,0.35);border-radius:8px;padding:7px 18px;">
<span style="font-family:'DM Sans',-apple-system,sans-serif;font-size:11px;
font-weight:600;letter-spacing:3px;color:${clr.gold};
text-transform:uppercase;">QR Master</span>
</td>
</tr>
</table>
<h1 style="margin:28px 0 0;font-family:'DM Serif Display',Georgia,serif;
font-size:34px;font-weight:400;line-height:1.2;color:#FFFFFF;">
A month in,<br>
<em style="color:${clr.gold};">${firstName}.</em>
</h1>
<p style="margin:16px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;color:rgba(255,255,255,0.45);">
Free plan · Day 30
</p>
</td>
</tr>
<!-- ── BODY ── -->
<tr>
<td style="padding:40px 48px 0;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.text};">
Your codes were scanned <em style="color:${clr.text};">${scanCount} time${scanCount !== 1 ? 's' : ''}</em> this month, across ${qrCount} code${qrCount !== 1 ? 's' : ''}. Someone stood in front of your printed material and used it.
</p>
<p style="margin:20px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.textSoft};">
What you cannot see yet is <em style="color:${clr.text};">which of them</em>. The free plan gives you a running total. It does not tell you which placement, which city, or which kind of device brought those scans in.
</p>
<p style="margin:20px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.textSoft};">
That is the difference between knowing a campaign worked and knowing which part of it did:
</p>
</td>
</tr>
<!-- ── FEATURES ── -->
<tr>
<td style="padding:28px 48px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<!-- Logo row -->
<tr>
<td style="padding:24px;background-color:${clr.pillBg};border-radius:12px;
border:1px solid ${clr.border};">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="width:36px;vertical-align:top;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="width:36px;height:36px;background-color:${clr.header};border-radius:10px;
text-align:center;vertical-align:middle;font-size:18px;color:${clr.gold};">
</td>
</tr>
</table>
</td>
<td style="padding-left:14px;vertical-align:top;padding-top:2px;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:15px;font-weight:600;color:${clr.text};letter-spacing:-0.2px;">Which placement earned the scan</p>
<p style="margin:6px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;color:${clr.textSoft};line-height:1.6;">
One code per flyer, poster or table tent, and the dashboard shows you which one people actually used. The others you can stop printing.
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Spacer -->
<tr><td style="height:16px;"></td></tr>
<!-- Colors row -->
<tr>
<td style="padding:24px;background-color:${clr.pillBg};border-radius:12px;
border:1px solid ${clr.border};">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="width:36px;vertical-align:top;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="width:36px;height:36px;background-color:${clr.header};border-radius:10px;
text-align:center;vertical-align:middle;font-size:18px;color:${clr.gold};">
</td>
</tr>
</table>
</td>
<td style="padding-left:14px;vertical-align:top;padding-top:2px;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:15px;font-weight:600;color:${clr.text};letter-spacing:-0.2px;">Device and location, not just a total</p>
<p style="margin:6px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;color:${clr.textSoft};line-height:1.6;">
See whether scans came from phones at the venue or desktops later, and which cities they came from. Enough to decide where the next print run goes.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- ── CTA ── -->
<tr>
<td style="padding:32px 48px 44px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td align="center">
<a href="${pricingUrl}"
style="display:inline-block;background-color:${clr.ctaBg};color:${clr.ctaText};
text-decoration:none;padding:18px 52px;border-radius:10px;
font-family:'DM Sans',-apple-system,sans-serif;font-size:15px;
font-weight:600;letter-spacing:0.3px;border:1px solid rgba(200,162,87,0.3);">
See the full breakdown &nbsp;→
</a>
</td>
</tr>
<tr>
<td align="center" style="padding-top:12px;">
<span style="font-family:'DM Sans',-apple-system,sans-serif;font-size:12px;
color:${clr.textMuted};">Your existing codes and their scan history stay exactly as they are</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- ── DIVIDER + SIGN-OFF ── -->
<tr><td style="padding:0 48px;"><div style="border-top:1px solid ${clr.border};"></div></td></tr>
<tr>
<td style="padding:28px 48px 40px;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;color:${clr.textSoft};line-height:1.7;">
If you ever want to talk through whether Pro makes sense for what you're building, just reply. Happy to help figure it out.
</p>
<p style="margin:16px 0 0;font-family:'DM Serif Display',Georgia,serif;
font-size:17px;color:${clr.text};">Timo</p>
<p style="margin:2px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:11px;color:${clr.textMuted};letter-spacing:0.5px;">FOUNDER, QR MASTER</p>
</td>
</tr>
`);
await transport.sendMail({
from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: `${firstName}, your codes were scanned ${scanCount} time${scanCount !== 1 ? 's' : ''} this month`,
html,
});
}
/**
* First Scan - sent the day after a user's first ever scan.
*
* The only email in the sequence whose trigger is not the calendar. Everything
* else fires on day 3, 7 or 30 whether or not anything happened; this one fires
* because something did. It is also the moment the positioning first pays off -
* a printed thing became a measurement - so it deliberately sells nothing. The
* CTA goes to the user's own data, not to pricing.
*/
export async function sendFirstScanEmail(
email: string,
name: string,
scan: { qrTitle: string; device?: string | null; country?: string | null; ts: Date }
) {
const transport = createSmtpTransport();
const firstName = name.split(' ')[0];
const analyticsUrl = `${appUrl}/analytics`;
const time = scan.ts.toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC',
});
// Only state what was actually recorded. Device and country can be missing,
// and inventing a clean sentence around absent data is exactly the habit this
// product is positioned against.
const details = [
`at ${time} UTC`,
scan.device ? `on ${scan.device.toLowerCase()}` : null,
scan.country ? `from ${scan.country}` : null,
]
.filter(Boolean)
.join(', ');
const html = emailShell('', `
<tr>
<td style="background-color:${clr.header};background-image:${dotGridPattern};
background-size:24px 24px;padding:44px 48px 40px;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:12px;font-weight:600;letter-spacing:1.6px;
text-transform:uppercase;color:${clr.gold};">QR Master</p>
<h1 style="margin:28px 0 0;font-family:'DM Serif Display',Georgia,serif;
font-size:34px;font-weight:400;line-height:1.2;color:#FFFFFF;">
Someone scanned it,<br>
<em style="color:${clr.gold};">${firstName}.</em>
</h1>
<p style="margin:16px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:14px;line-height:1.6;color:rgba(255,255,255,0.62);">
First scan &middot; ${scan.qrTitle}
</p>
</td>
</tr>
<tr>
<td style="padding:40px 48px 0;">
<p style="margin:0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.text};">
Your code "${scan.qrTitle}" was scanned for the first time ${details}.
</p>
<p style="margin:20px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.textSoft};">
Somebody stood in front of something you made and used it. That is the
part a static code can never tell you.
</p>
<p style="margin:20px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:16px;line-height:1.75;color:${clr.textSoft};">
Every scan from here on is counted. In a week or two you will be able to
see when they cluster - and whether the spot you chose was the right one.
</p>
</td>
</tr>
<tr>
<td style="padding:32px 48px 48px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="background-color:${clr.ctaBg};border-radius:6px;">
<a href="${analyticsUrl}"
style="display:inline-block;padding:15px 30px;
font-family:'DM Sans',-apple-system,sans-serif;
font-size:15px;font-weight:600;color:${clr.ctaText};
text-decoration:none;letter-spacing:-0.1px;">
See the scans &nbsp;&rarr;
</a>
</td>
</tr>
</table>
<p style="margin:24px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:15px;line-height:1.7;color:${clr.textSoft};">
If the destination needs to change, you can edit it without touching the
printed code. That is what it is for.
</p>
<p style="margin:28px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:15px;line-height:1.7;color:${clr.text};">
Timo
</p>
<p style="margin:2px 0 0;font-family:'DM Sans',-apple-system,sans-serif;
font-size:12px;font-weight:600;letter-spacing:1.2px;
text-transform:uppercase;color:${clr.textMuted};">
Founder, QR Master
</p>
</td>
</tr>
`);
await transport.sendMail({
from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: 'Your QR code was just scanned for the first time',
html,
});
}