Copy overhaul + qr designs

This commit is contained in:
2026-07-27 17:54:59 +02:00
parent 033bc7e29d
commit 70d97aa970
144 changed files with 23107 additions and 1699 deletions

View File

@@ -154,7 +154,7 @@ export async function sendPasswordResetEmail(email: string, resetToken: string)
Secure QR Code Analytics & Management
</p>
<p style="margin: 0; color: #999999; font-size: 12px;">
© 2026 QR Master. All rights reserved.
© 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.
@@ -287,7 +287,7 @@ export async function sendNewsletterWelcomeEmail(email: string) {
⏰ 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.
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>
@@ -297,7 +297,7 @@ export async function sendNewsletterWelcomeEmail(email: string) {
💡 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.
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 →
@@ -310,7 +310,7 @@ export async function sendNewsletterWelcomeEmail(email: string) {
</p>
<p style="margin: 20px 0 0 0; color: #555555; font-size: 16px; line-height: 1.7;">
<strong> Timo 👋</strong><br>
<strong>- Timo 👋</strong><br>
<span style="color: #888888; font-size: 14px;">Founder, QR Master</span>
</p>
</td>
@@ -326,7 +326,7 @@ export async function sendNewsletterWelcomeEmail(email: string) {
<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.
© 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.
@@ -461,7 +461,7 @@ export async function sendAIFeatureLaunchEmail(email: string) {
<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 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>
@@ -483,10 +483,10 @@ export async function sendAIFeatureLaunchEmail(email: string) {
<!-- 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.
💬 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>
<strong>- Timo 👋</strong><br>
<span style="color: #888888; font-size: 13px;">Founder, QR Master</span>
</p>
</div>
@@ -506,7 +506,7 @@ export async function sendAIFeatureLaunchEmail(email: string) {
<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.
© 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.
@@ -535,29 +535,29 @@ export async function sendAIFeatureLaunchEmail(email: string) {
}
// ---------------------------------------------------------------------------
// SMTP Transport (nodemailer) used for retention / welcome emails
// 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,
},
});
}
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';
@@ -638,7 +638,7 @@ function emailShell(headExtra: string, bodyContent: string): string {
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)
* Welcome Email - sent immediately on signup (Day 0)
*/
export async function sendWelcomeEmail(email: string, name: string) {
const transport = createSmtpTransport();
@@ -797,7 +797,7 @@ export async function sendWelcomeEmail(email: string, name: string) {
<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.
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;">
@@ -806,7 +806,7 @@ export async function sendWelcomeEmail(email: string, name: string) {
<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;">
<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>
@@ -838,7 +838,7 @@ export async function sendWelcomeEmail(email: string, name: string) {
}
/**
* Activation Nudge sent on Day 3 if user has 0 QR codes
* Activation Nudge - sent on Day 3 if user has 0 QR codes
*/
export async function sendActivationNudgeEmail(email: string, name: string) {
const transport = createSmtpTransport();
@@ -847,7 +847,7 @@ export async function sendActivationNudgeEmail(email: string, name: string) {
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: '02', label: 'Customize the design', sub: 'colors, logo, frame label - optional' },
{ n: '03', label: 'Download & use', sub: 'PNG for screen, SVG for print' },
];
@@ -905,7 +905,7 @@ export async function sendActivationNudgeEmail(email: string, name: string) {
<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>
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;
@@ -969,13 +969,13 @@ export async function sendActivationNudgeEmail(email: string, name: string) {
from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net',
to: email,
subject: "You haven't made one yet",
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
* 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();
@@ -990,18 +990,18 @@ export async function sendUpgradeNudgeEmail(email: string, name: string, qrCount
},
{
label: 'Logo in QR code',
free: '',
free: '-',
pro: 'Your logo, centered',
},
{
label: 'Brand colors',
free: '',
pro: 'Any color',
label: 'Module shapes',
free: '-',
pro: '4 shapes + eye styles',
},
{
label: 'CSV export',
free: '',
pro: '',
label: 'Scan detail',
free: 'Total scans',
pro: 'Device, location, time',
},
];
@@ -1039,13 +1039,13 @@ export async function sendUpgradeNudgeEmail(email: string, name: string, qrCount
<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>
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 · Day 7
Free plan · ${qrCount} of 3 dynamic codes active
</p>
</td>
</tr>
@@ -1055,11 +1055,11 @@ export async function sendUpgradeNudgeEmail(email: string, name: string, qrCount
<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.
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 removes the ceiling — and adds custom branding your free codes never have:
Pro raises it to 50, and shows you which placement actually earned the scan:
</p>
</td>
</tr>
@@ -1113,7 +1113,7 @@ export async function sendUpgradeNudgeEmail(email: string, name: string, qrCount
<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>
color:${clr.textMuted};">Your free plan stays active - no pressure</span>
</td>
</tr>
</table>
@@ -1140,15 +1140,20 @@ export async function sendUpgradeNudgeEmail(email: string, name: string, qrCount
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`,
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
* 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) {
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];
@@ -1187,15 +1192,15 @@ export async function sendThirtyDayNudgeEmail(email: string, name: string, qrCou
<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.
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};">
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.
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};">
Pro fixes that. Two things it unlocks that free never will:
That is the difference between knowing a campaign worked and knowing which part of it did:
</p>
</td>
</tr>
@@ -1222,10 +1227,10 @@ export async function sendThirtyDayNudgeEmail(email: string, name: string, qrCou
</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>
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;">
Upload once. Every code you make automatically carries your brand mark — menus, flyers, packaging, wherever.
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>
@@ -1252,10 +1257,10 @@ export async function sendThirtyDayNudgeEmail(email: string, name: string, qrCou
</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>
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;">
Match your QR codes to your brand palette. Looks intentional. Scans the same.
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>
@@ -1277,14 +1282,14 @@ export async function sendThirtyDayNudgeEmail(email: string, name: string, qrCou
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 &nbsp;→
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 keep working — nothing breaks</span>
color:${clr.textMuted};">Your existing codes and their scan history stay exactly as they are</span>
</td>
</tr>
</table>
@@ -1311,7 +1316,122 @@ export async function sendThirtyDayNudgeEmail(email: string, name: string, qrCou
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`,
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,
});
}