/** * 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 ', replyTo: 'support@qrmaster.net', to: email, subject: '๐Ÿ” Reset Your QR Master Password (Expires in 1 Hour)', html: ` Reset Your Password - QR Master
๐Ÿ”’ SECURITY ALERT

Password Reset Request

Someone requested to reset your password

Hi there,

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:

๐Ÿ” Reset My Password โ†’

โฑ๏ธ This link expires in 60 minutes

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.

๐Ÿ”— Button not working? Copy and paste this link into your browser:

${resetUrl}

โ“ Didn't request this?

โœ… 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 support@qrmaster.net

QR Master

Secure QR Code Analytics & Management

ยฉ 2026 QR Master. All rights reserved.

This is an automated security email. Please do not reply.

`, }); 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 ', replyTo: 'support@qrmaster.net', to: email, subject: '๐ŸŽ‰ You\'re In! Here\'s What Happens Next (AI QR Features)', html: ` Welcome to QR Master AI Waitlist
โœจ EARLY ACCESS

You're on the List! ๐Ÿš€

Get ready for AI-powered QR codes that work smarter, not harder

Hey there! ๐Ÿ‘‹

Thanks for joining the waitlist for QR Master's AI-powered features. You're among the first to know when we launch something that'll completely change how you create and manage QR codes.

No fluff, no spam. Just a heads-up when these features go live.

๐ŸŽฏ What You Can Expect

โšก Smart QR Generation

AI analyzes your content and automatically suggests the optimal QR design, colors, and format for maximum scans. No more guessing what works.

๐Ÿ“Š Predictive Analytics

See scan forecasts, detect unusual patterns, and ask questions about your data in plain English. Your analytics become conversations, not spreadsheets.

๐Ÿค– Auto-Organization

Your QR codes automatically get categorized, tagged, and organized. Spend less time managing, more time creating.

๐ŸŽจ AI Design Studio

Generate unique, on-brand QR code designs in seconds. Just describe what you want, AI handles the rest.

โฐ When Will This Happen?

We're in active development and testing. You'll get an email the moment these features go live - no waiting, no wondering. We respect your inbox.

๐Ÿ’ก Want to Get Started Now?

Our current platform already helps teams create, track, and manage dynamic QR codes. No AI needed - just powerful tools that work today.

๐Ÿš€ Try QR Master Free โ†’

๐Ÿ’Œ Thanks for trusting us with your inbox. We'll make it worth it.

- Timo ๐Ÿ‘‹
Founder, QR Master

www.qrmaster.net

ยฉ 2026 QR Master. All rights reserved.

You're receiving this because you signed up for AI feature notifications.

`, }); 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 ', replyTo: 'support@qrmaster.net', to: email, subject: '๐Ÿš€ They\'re Live! Your AI QR Features Are Ready', html: ` AI Features Are Live - QR Master
๐ŸŽ‰
โœจ NOW LIVE

AI Features Are Here! ๐Ÿš€

Ready to use in your dashboard

Remember when you signed up to be notified about our AI features?

Well, today's that day. ๐ŸŽฏ

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.

โœจ What's New in Your Dashboard

โšก Smart QR Generation

AI analyzes your content and suggests optimal designs automatically. Just paste your link, we handle the optimization.

๐Ÿ“Š Predictive Analytics

Ask questions about your scan data in plain English. "Which campaign performed best last month?" Done.

๐Ÿค– Auto-Organization

QR codes automatically categorized and tagged. Your library stays organized without manual work.

๐ŸŽจ AI Design Studio

Generate custom QR designs by describing what you want. "Make it modern and blue" โ†’ Done in 3 seconds.

๐ŸŽฏ How to Get Started

  1. ๐Ÿ” Log in to your QR Master account
  2. ๐Ÿ‘€ Look for the "โœจ AI" badge on supported features
  3. โœ๏ธ Try creating a QR code - you'll see AI suggestions automatically
  4. ๐Ÿ“ˆ Check your Analytics tab for the new AI query interface
โœจ Try AI Features Now โ†’

โœ… Available on all plans โ€ข โšก No extra setup required

๐Ÿ’ฌ 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 - hit reply. I read every email.

- Timo ๐Ÿ‘‹
Founder, QR Master

www.qrmaster.net โ€ข Dashboard โ€ข Help

ยฉ 2026 QR Master. All rights reserved.

You received this because you subscribed to AI feature launch notifications.

`, }); 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 = ` `; function emailShell(headExtra: string, bodyContent: string): string { return ` ${webFontHead} ${headExtra}
${bodyContent}

www.qrmaster.net  ยท  support@qrmaster.net

ยฉ 2026 QR Master. You're receiving this because you created an account.

`; } // 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('', `
QR Master

Welcome,
${firstName}.

The physical world is now your canvas.

Beautiful QR Code Experience

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.

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).

To give you the perfect start, we've loaded your account with everything you need:

3
Free Codes
90s
To Create
โˆž
No Expiry
Design your first QR code  โ†’
No credit card required. Zero commitment.

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.

Timo

Timo

Founder, QR Master

`); await transport.sendMail({ from: 'Timo from QR Master ', 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 => `
${s.n}
${s.label}
${s.sub}
`).join(''); const html = emailShell('', `
QR Master

You haven't made one yet,
${firstName}.

3 days since signup ยท 0 QR codes created

Your 3 free dynamic QR codes are still there. Unused.
Dynamic means: one code, update the link anytime, every scan tracked.

Here's all it takes:

${stepsHtml}
Make my first QR code  โ†’
Free forever ยท no card needed

If something stopped you from getting started, just reply. I'll help directly.

Timo

FOUNDER, QR MASTER

`); await transport.sendMail({ from: 'Timo from QR Master ', 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) => ` ${f.label} ${f.free} ${f.pro} `).join(''); const html = emailShell('', `
QR Master

You are out of slots,
${firstName}.

Free plan ยท ${qrCount} of 3 dynamic codes active

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.

Pro raises it to 50, and shows you which placement actually earned the scan:

${rowsHtml}
Feature Free Pro
See Pro plan  โ†’
Your free plan stays active - no pressure

If you have questions about what Pro unlocks, just reply here.

Timo

FOUNDER, QR MASTER

`); await transport.sendMail({ from: 'Timo from QR Master ', 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('', `
QR Master

A month in,
${firstName}.

Free plan ยท Day 30

Your codes were scanned ${scanCount} time${scanCount !== 1 ? 's' : ''} this month, across ${qrCount} code${qrCount !== 1 ? 's' : ''}. Someone stood in front of your printed material and used it.

What you cannot see yet is which of them. 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.

That is the difference between knowing a campaign worked and knowing which part of it did:

โ—†

Which placement earned the scan

One code per flyer, poster or table tent, and the dashboard shows you which one people actually used. The others you can stop printing.

โ—

Device and location, not just a total

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.

See the full breakdown  โ†’
Your existing codes and their scan history stay exactly as they are

If you ever want to talk through whether Pro makes sense for what you're building, just reply. Happy to help figure it out.

Timo

FOUNDER, QR MASTER

`); await transport.sendMail({ from: 'Timo from QR Master ', 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('', `

QR Master

Someone scanned it,
${firstName}.

First scan · ${scan.qrTitle}

Your code "${scan.qrTitle}" was scanned for the first time ${details}.

Somebody stood in front of something you made and used it. That is the part a static code can never tell you.

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.

See the scans  →

If the destination needs to change, you can edit it without touching the printed code. That is what it is for.

Timo

Founder, QR Master

`); await transport.sendMail({ from: 'Timo from QR Master ', replyTo: 'support@qrmaster.net', to: email, subject: 'Your QR code was just scanned for the first time', html, }); }