Email retention
This commit is contained in:
774
src/lib/email.ts
774
src/lib/email.ts
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
@@ -532,3 +533,776 @@ export async function sendAIFeatureLaunchEmail(email: string) {
|
||||
throw new Error('Failed to send AI feature launch email');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMTP Transport (nodemailer) — used for retention / welcome emails
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createSmtpTransport() {
|
||||
return nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || 'smtp.qrmaster.net',
|
||||
port: parseInt(process.env.SMTP_PORT || '465', 10),
|
||||
secure: true, // port 465 = SSL
|
||||
auth: {
|
||||
user: process.env.SMTP_USER || 'timo@qrmaster.net',
|
||||
pass: process.env.SMTP_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>
|
||||
·
|
||||
<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 →
|
||||
</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}/favicon.svg" width="32" height="32" alt="Timo" style="display:inline-block; vertical-align:middle;">
|
||||
</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 →
|
||||
</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: "You haven't made one yet",
|
||||
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: 'Brand colors',
|
||||
free: '—',
|
||||
pro: 'Any color',
|
||||
},
|
||||
{
|
||||
label: 'CSV export',
|
||||
free: '✓',
|
||||
pro: '✓',
|
||||
},
|
||||
];
|
||||
|
||||
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;">
|
||||
${qrCount} of 3 free codes used,<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 7
|
||||
</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};">
|
||||
The free plan gives you 3 dynamic QR codes. That's enough to start — not enough to scale. When you hit the limit, every new campaign means replacing 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 removes the ceiling — and adds custom branding your free codes never have:
|
||||
</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 →
|
||||
</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're ${qrCount >= 3 ? 'at' : `${3 - qrCount} away from`} 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) {
|
||||
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};">
|
||||
You've created ${qrCount} QR code${qrCount !== 1 ? 's' : ''} in your first month. That tells me you're actually using this — not just signing up to forget 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};">
|
||||
The one thing I hear most from Pro users who switched after a few weeks: <em style="color:${clr.text};">they wish they'd added their brand sooner.</em> Every code they printed on free didn't have their logo. Every flyer had a generic black pattern instead of their colors.
|
||||
</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};">
|
||||
Pro fixes that. Two things it unlocks that free never will:
|
||||
</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;">Your logo, inside every QR code</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;">
|
||||
Upload once. Every code you make automatically carries your brand mark — menus, flyers, packaging, wherever.
|
||||
</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;">Brand colors — not just black</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;">
|
||||
Match your QR codes to your brand palette. Looks intentional. Scans the same.
|
||||
</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);">
|
||||
Add my brand to QR codes →
|
||||
</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 keep working — nothing breaks</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}, a month of QR codes — one upgrade worth making`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user