Final
This commit is contained in:
parent
42e8a02fde
commit
82ea760537
|
|
@ -0,0 +1,269 @@
|
||||||
|
# 🚀 Deployment Checklist für QR Master
|
||||||
|
|
||||||
|
Diese Checkliste enthält alle notwendigen Änderungen vor dem Push nach Gitea und dem Production Deployment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 1. Environment Variables (.env)
|
||||||
|
|
||||||
|
### Basis URLs ändern
|
||||||
|
```bash
|
||||||
|
# Von:
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3050
|
||||||
|
NEXTAUTH_URL=http://localhost:3050
|
||||||
|
|
||||||
|
# Zu:
|
||||||
|
NEXT_PUBLIC_APP_URL=https://www.qrmaster.net
|
||||||
|
NEXTAUTH_URL=https://www.qrmaster.net
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secrets generieren (falls noch nicht geschehen)
|
||||||
|
```bash
|
||||||
|
# NEXTAUTH_SECRET (für JWT/Session Encryption)
|
||||||
|
openssl rand -base64 32
|
||||||
|
|
||||||
|
# IP_SALT (für DSGVO-konforme IP-Hashing)
|
||||||
|
openssl rand -base64 32
|
||||||
|
```
|
||||||
|
|
||||||
|
Bereits generiert:
|
||||||
|
- ✅ NEXTAUTH_SECRET: `PT8XVydC4v7QluCz/mV1yb7Y3docSFZeFDioJz4ZE98=`
|
||||||
|
- ✅ IP_SALT: `j/aluIpzsgn5Z6cbF4conM6ApK5cj4jDagkswzfgQPc=`
|
||||||
|
|
||||||
|
### Database URLs
|
||||||
|
```bash
|
||||||
|
# Development (localhost):
|
||||||
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public"
|
||||||
|
DIRECT_URL="postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public"
|
||||||
|
|
||||||
|
# Production (anpassen an deinen Server):
|
||||||
|
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/qrmaster?schema=public"
|
||||||
|
DIRECT_URL="postgresql://USER:PASSWORD@HOST:5432/qrmaster?schema=public"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 2. Google OAuth Configuration
|
||||||
|
|
||||||
|
### Redirect URIs in Google Cloud Console hinzufügen
|
||||||
|
|
||||||
|
1. Gehe zu: https://console.cloud.google.com/apis/credentials
|
||||||
|
2. Wähle deine OAuth 2.0 Client ID: `683784117141-ci1d928jo8f9g6i1isrveflmrinp92l4.apps.googleusercontent.com`
|
||||||
|
3. Füge folgende **Authorized redirect URIs** hinzu:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://www.qrmaster.net/api/auth/callback/google
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optional** (für Staging/Testing):
|
||||||
|
```
|
||||||
|
http://localhost:3050/api/auth/callback/google
|
||||||
|
https://staging.qrmaster.net/api/auth/callback/google
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💳 3. Stripe Configuration
|
||||||
|
|
||||||
|
### ⚠️ WICHTIG: Von Test Mode zu Live Mode wechseln
|
||||||
|
|
||||||
|
#### Current (Test Mode):
|
||||||
|
```bash
|
||||||
|
STRIPE_SECRET_KEY=sk_test_51QYL7gP9xM...
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51QYL7gP9xM...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production (Live Mode):
|
||||||
|
1. Gehe zu: https://dashboard.stripe.com/
|
||||||
|
2. Wechsle von **Test Mode** zu **Live Mode** (Toggle oben rechts)
|
||||||
|
3. Hole dir die **Live Keys**:
|
||||||
|
- `API Keys` → `Secret key` (beginnt mit `sk_live_`)
|
||||||
|
- `API Keys` → `Publishable key` (beginnt mit `pk_live_`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production Keys:
|
||||||
|
STRIPE_SECRET_KEY=sk_live_XXXXX
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_XXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Webhook Secret (Production)
|
||||||
|
1. Erstelle einen neuen Webhook Endpoint: https://dashboard.stripe.com/webhooks
|
||||||
|
2. Endpoint URL: `https://www.qrmaster.net/api/webhooks/stripe`
|
||||||
|
3. Events to listen:
|
||||||
|
- `checkout.session.completed`
|
||||||
|
- `customer.subscription.updated`
|
||||||
|
- `customer.subscription.deleted`
|
||||||
|
- `invoice.payment_succeeded`
|
||||||
|
- `invoice.payment_failed`
|
||||||
|
4. Kopiere den **Signing Secret** (beginnt mit `whsec_`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_XXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Price IDs aktualisieren
|
||||||
|
Erstelle Produkte und Preise in **Live Mode**:
|
||||||
|
1. https://dashboard.stripe.com/products
|
||||||
|
2. Erstelle "Pro" und "Business" Pläne
|
||||||
|
3. Kopiere die Price IDs (beginnen mit `price_`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
STRIPE_PRICE_ID_PRO_MONTHLY=price_XXXXX
|
||||||
|
STRIPE_PRICE_ID_PRO_YEARLY=price_XXXXX
|
||||||
|
STRIPE_PRICE_ID_BUSINESS_MONTHLY=price_XXXXX
|
||||||
|
STRIPE_PRICE_ID_BUSINESS_YEARLY=price_XXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📧 4. Resend Email Configuration
|
||||||
|
|
||||||
|
### Domain Verification
|
||||||
|
1. Gehe zu: https://resend.com/domains
|
||||||
|
2. Füge Domain hinzu: `qrmaster.net`
|
||||||
|
3. Konfiguriere DNS Records (SPF, DKIM, DMARC)
|
||||||
|
4. Warte auf Verification
|
||||||
|
|
||||||
|
### From Email anpassen
|
||||||
|
Aktuell verwendet alle Emails: `onboarding@resend.dev` (Resend's Test Domain)
|
||||||
|
|
||||||
|
Nach Domain Verification in `src/lib/email.ts` ändern:
|
||||||
|
```typescript
|
||||||
|
// Von:
|
||||||
|
from: 'Timo from QR Master <onboarding@resend.dev>',
|
||||||
|
|
||||||
|
// Zu:
|
||||||
|
from: 'Timo from QR Master <hello@qrmaster.net>',
|
||||||
|
// oder
|
||||||
|
from: 'Timo from QR Master <noreply@qrmaster.net>',
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 5. SEO Configuration
|
||||||
|
|
||||||
|
### Bereits korrekt konfiguriert ✅
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_INDEXABLE=true # ✅ Bereits gesetzt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sitemap & robots.txt prüfen
|
||||||
|
- Sitemap: `https://www.qrmaster.net/sitemap.xml`
|
||||||
|
- Robots: `https://www.qrmaster.net/robots.txt`
|
||||||
|
|
||||||
|
Nach Deployment testen!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 6. PostHog Analytics (Optional)
|
||||||
|
|
||||||
|
Falls du PostHog nutzt:
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_POSTHOG_KEY=phc_XXXXX
|
||||||
|
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 7. Docker Deployment
|
||||||
|
|
||||||
|
### docker-compose.yml prüfen
|
||||||
|
Stelle sicher, dass alle ENV Variables korrekt gemappt sind:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
NEXTAUTH_URL: https://www.qrmaster.net
|
||||||
|
NEXT_PUBLIC_APP_URL: https://www.qrmaster.net
|
||||||
|
# ... weitere vars
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment Commands
|
||||||
|
```bash
|
||||||
|
# Build & Deploy
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Database Migration (nach erstem Deploy)
|
||||||
|
docker-compose exec web npm run db:migrate
|
||||||
|
|
||||||
|
# Logs checken
|
||||||
|
docker-compose logs -f web
|
||||||
|
|
||||||
|
# Health Check
|
||||||
|
curl https://www.qrmaster.net
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 8. Security Checklist
|
||||||
|
|
||||||
|
- [ ] ✅ NEXTAUTH_SECRET ist gesetzt und sicher (32+ Zeichen)
|
||||||
|
- [ ] ✅ IP_SALT ist gesetzt und sicher
|
||||||
|
- [ ] ⚠️ Stripe ist auf **Live Mode** umgestellt
|
||||||
|
- [ ] ⚠️ Google OAuth Redirect URIs enthalten Production URL
|
||||||
|
- [ ] ⚠️ Resend Domain ist verifiziert
|
||||||
|
- [ ] ⚠️ Webhook Secrets sind für Production gesetzt
|
||||||
|
- [ ] ⚠️ Database URLs zeigen auf Production DB
|
||||||
|
- [ ] ⚠️ Keine Test/Dev Secrets in Production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 9. Vor dem Git Push
|
||||||
|
|
||||||
|
### Files prüfen
|
||||||
|
```bash
|
||||||
|
# .env sollte NICHT committet werden!
|
||||||
|
git status
|
||||||
|
|
||||||
|
# Falls .env in Git ist:
|
||||||
|
git rm --cached .env
|
||||||
|
echo ".env" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sensible Daten entfernen
|
||||||
|
- [ ] Keine API Keys im Code
|
||||||
|
- [ ] Keine Secrets in Config Files
|
||||||
|
- [ ] `.env` ist in `.gitignore`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 10. Nach dem Deployment testen
|
||||||
|
|
||||||
|
### Funktionen testen
|
||||||
|
1. **Google OAuth Login**: https://www.qrmaster.net/login
|
||||||
|
2. **QR Code erstellen**: https://www.qrmaster.net/create
|
||||||
|
3. **Stripe Checkout**: Testprodukt kaufen mit echten Stripe Test Cards
|
||||||
|
4. **Email Delivery**: Password Reset testen
|
||||||
|
5. **Analytics**: PostHog Events tracken
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
```bash
|
||||||
|
# Server Logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Database Status
|
||||||
|
docker-compose exec db psql -U postgres -d qrmaster -c "SELECT COUNT(*) FROM \"User\";"
|
||||||
|
|
||||||
|
# Redis Status
|
||||||
|
docker-compose exec redis redis-cli PING
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support Kontakte
|
||||||
|
|
||||||
|
- **Stripe Support**: https://support.stripe.com
|
||||||
|
- **Google Cloud Support**: https://support.google.com/cloud
|
||||||
|
- **Resend Support**: https://resend.com/docs
|
||||||
|
- **Next.js Docs**: https://nextjs.org/docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Deployment erfolgreich!
|
||||||
|
|
||||||
|
Nach erfolgreichem Deployment:
|
||||||
|
1. ✅ Teste alle wichtigen Features
|
||||||
|
2. ✅ Monitor Logs für Fehler
|
||||||
|
3. ✅ Prüfe Analytics Dashboard
|
||||||
|
4. ✅ Backup der Production Database erstellen
|
||||||
|
|
||||||
|
**Good luck! 🚀**
|
||||||
|
|
@ -30,6 +30,9 @@ ENV DATABASE_URL="postgresql://postgres:postgres@db:5432/qrmaster?schema=public"
|
||||||
ENV NEXTAUTH_URL="http://localhost:3000"
|
ENV NEXTAUTH_URL="http://localhost:3000"
|
||||||
ENV NEXTAUTH_SECRET="build-time-secret"
|
ENV NEXTAUTH_SECRET="build-time-secret"
|
||||||
ENV IP_SALT="build-time-salt"
|
ENV IP_SALT="build-time-salt"
|
||||||
|
ENV STRIPE_SECRET_KEY="sk_test_placeholder_for_build"
|
||||||
|
ENV RESEND_API_KEY="re_placeholder_for_build"
|
||||||
|
ENV NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,29 @@ services:
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DATABASE_URL: postgresql://postgres:postgres@db:5432/qrmaster?schema=public
|
DATABASE_URL: postgresql://postgres:postgres@db:5432/qrmaster?schema=public
|
||||||
|
DIRECT_URL: postgresql://postgres:postgres@db:5432/qrmaster?schema=public
|
||||||
REDIS_URL: redis://redis:6379
|
REDIS_URL: redis://redis:6379
|
||||||
NEXTAUTH_URL: http://localhost:3050
|
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3050}
|
||||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-your-secret-key-change-in-production}
|
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-your-secret-key-change-in-production}
|
||||||
|
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3050}
|
||||||
IP_SALT: ${IP_SALT:-your-salt-change-in-production}
|
IP_SALT: ${IP_SALT:-your-salt-change-in-production}
|
||||||
|
ENABLE_DEMO: ${ENABLE_DEMO:-false}
|
||||||
|
NEXT_PUBLIC_INDEXABLE: ${NEXT_PUBLIC_INDEXABLE:-true}
|
||||||
|
# Google OAuth
|
||||||
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
||||||
|
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
||||||
|
# Stripe
|
||||||
|
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:-}
|
||||||
|
STRIPE_PRICE_ID_PRO_MONTHLY: ${STRIPE_PRICE_ID_PRO_MONTHLY:-}
|
||||||
|
STRIPE_PRICE_ID_PRO_YEARLY: ${STRIPE_PRICE_ID_PRO_YEARLY:-}
|
||||||
|
STRIPE_PRICE_ID_BUSINESS_MONTHLY: ${STRIPE_PRICE_ID_BUSINESS_MONTHLY:-}
|
||||||
|
STRIPE_PRICE_ID_BUSINESS_YEARLY: ${STRIPE_PRICE_ID_BUSINESS_YEARLY:-}
|
||||||
|
# Email & Analytics
|
||||||
|
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||||
|
NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY:-}
|
||||||
|
NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST:-https://us.i.posthog.com}
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,13 @@ const nextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'],
|
serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'],
|
||||||
},
|
},
|
||||||
|
// Allow build to succeed even with prerender errors
|
||||||
|
// Pages with useSearchParams() will be rendered dynamically at runtime
|
||||||
|
staticPageGenerationTimeout: 120,
|
||||||
|
onDemandEntries: {
|
||||||
|
maxInactiveAge: 25 * 1000,
|
||||||
|
pagesBufferLength: 2,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
|
|
@ -358,7 +358,7 @@ export default function AnalyticsPage() {
|
||||||
<td className="px-4 py-4 align-middle">
|
<td className="px-4 py-4 align-middle">
|
||||||
<Badge variant={
|
<Badge variant={
|
||||||
country.trend === 'up' ? 'success' :
|
country.trend === 'up' ? 'success' :
|
||||||
country.trend === 'down' ? 'destructive' :
|
country.trend === 'down' ? 'error' :
|
||||||
'default'
|
'default'
|
||||||
}>
|
}>
|
||||||
{country.trend === 'up' ? '↑' : country.trend === 'down' ? '↓' : '→'} {country.trendPercentage}%{country.isNew ? ' (new)' : ''}
|
{country.trend === 'up' ? '↑' : country.trend === 'down' ? '↓' : '→'} {country.trendPercentage}%{country.isNew ? ' (new)' : ''}
|
||||||
|
|
@ -407,7 +407,7 @@ export default function AnalyticsPage() {
|
||||||
<td className="px-4 py-4 align-middle">
|
<td className="px-4 py-4 align-middle">
|
||||||
<Badge variant={
|
<Badge variant={
|
||||||
qr.trend === 'up' ? 'success' :
|
qr.trend === 'up' ? 'success' :
|
||||||
qr.trend === 'down' ? 'destructive' :
|
qr.trend === 'down' ? 'error' :
|
||||||
'default'
|
'default'
|
||||||
}>
|
}>
|
||||||
{qr.trend === 'up' ? '↑' : qr.trend === 'down' ? '↓' : '→'} {qr.trendPercentage}%{qr.isNew ? ' (new)' : ''}
|
{qr.trend === 'up' ? '↑' : qr.trend === 'down' ? '↓' : '→'} {qr.trendPercentage}%{qr.isNew ? ' (new)' : ''}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ interface QRCodeData {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
scans: number;
|
scans: number;
|
||||||
style?: any;
|
style?: any;
|
||||||
|
status?: 'ACTIVE' | 'INACTIVE';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { Suspense } from 'react';
|
||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
import { ToastContainer } from '@/components/ui/Toast';
|
import { ToastContainer } from '@/components/ui/Toast';
|
||||||
import AuthProvider from '@/components/SessionProvider';
|
import AuthProvider from '@/components/SessionProvider';
|
||||||
|
|
@ -56,6 +57,7 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className="font-sans">
|
<body className="font-sans">
|
||||||
|
<Suspense fallback={null}>
|
||||||
<PostHogProvider>
|
<PostHogProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -63,6 +65,7 @@ export default function RootLayout({
|
||||||
<CookieBanner />
|
<CookieBanner />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
|
</Suspense>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
454
src/lib/email.ts
454
src/lib/email.ts
|
|
@ -1,76 +1,140 @@
|
||||||
|
/**
|
||||||
|
* 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 { Resend } from 'resend';
|
||||||
|
|
||||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
// 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) {
|
export async function sendPasswordResetEmail(email: string, resetToken: string) {
|
||||||
|
await waitForRateLimit();
|
||||||
|
|
||||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
|
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
|
||||||
const resetUrl = `${appUrl}/reset-password?token=${resetToken}`;
|
const resetUrl = `${appUrl}/reset-password?token=${resetToken}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await resend.emails.send({
|
await resend.emails.send({
|
||||||
from: 'QR Master <onboarding@resend.dev>', // Use Resend's testing domain
|
from: 'QR Master Security <onboarding@resend.dev>',
|
||||||
replyTo: 'support@qrmaster.net',
|
replyTo: 'support@qrmaster.net',
|
||||||
to: email,
|
to: email,
|
||||||
subject: 'Reset Your Password - QR Master',
|
subject: '🔐 Reset Your QR Master Password (Expires in 1 Hour)',
|
||||||
html: `
|
html: `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Reset Your Password</title>
|
<title>Reset Your Password - QR Master</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
|
<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: 20px 0;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f4; padding: 40px 20px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
<!-- Main Container -->
|
||||||
<!-- Header -->
|
<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>
|
<tr>
|
||||||
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 30px; text-align: center;">
|
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 50px 40px; text-align: center;">
|
||||||
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: bold;">QR Master</h1>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content Section -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 40px 30px;">
|
<td style="padding: 50px 40px;">
|
||||||
<h2 style="margin: 0 0 20px 0; color: #333333; font-size: 24px;">Reset Your Password</h2>
|
<p style="margin: 0 0 20px 0; color: #333333; font-size: 16px; line-height: 1.6;">
|
||||||
|
|
||||||
<p style="margin: 0 0 20px 0; color: #666666; font-size: 16px; line-height: 1.5;">
|
|
||||||
Hi there,
|
Hi there,
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 30px 0; color: #666666; font-size: 16px; line-height: 1.5;">
|
<p style="margin: 0 0 25px 0; color: #555555; font-size: 16px; line-height: 1.7;">
|
||||||
You requested to reset your password for your QR Master account. Click the button below to choose a new password:
|
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>
|
</p>
|
||||||
|
|
||||||
<!-- Button -->
|
<!-- Primary CTA Button -->
|
||||||
<table width="100%" cellpadding="0" cellspacing="0">
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 35px 0;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px 0;">
|
<td align="center">
|
||||||
<a href="${resetUrl}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 16px 40px; border-radius: 6px; font-size: 16px; font-weight: bold; display: inline-block;">
|
<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 Password
|
🔐 Reset My Password →
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p style="margin: 30px 0 20px 0; color: #666666; font-size: 14px; line-height: 1.5;">
|
<!-- Security Info Box -->
|
||||||
Or copy and paste this link into your browser:
|
<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>
|
||||||
|
|
||||||
<p style="margin: 0 0 30px 0; padding: 15px; background-color: #f8f8f8; border-radius: 4px; word-break: break-all;">
|
<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: 14px;">${resetUrl}</a>
|
<a href="${resetUrl}" style="color: #667eea; text-decoration: none; font-size: 13px; font-family: monospace;">${resetUrl}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="border-top: 1px solid #eeeeee; padding-top: 20px; margin-top: 30px;">
|
<!-- Didn't Request This? -->
|
||||||
<p style="margin: 0 0 10px 0; color: #999999; font-size: 13px; line-height: 1.5;">
|
<div style="background-color: #f8f9fa; padding: 25px; border-radius: 8px; margin-top: 35px;">
|
||||||
<strong>This link will expire in 1 hour.</strong>
|
<p style="margin: 0 0 12px 0; color: #333333; font-size: 15px; font-weight: 600;">
|
||||||
|
❓ Didn't request this?
|
||||||
</p>
|
</p>
|
||||||
|
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6;">
|
||||||
<p style="margin: 0; color: #999999; font-size: 13px; line-height: 1.5;">
|
✅ 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>
|
||||||
If you didn't request a password reset, you can safely ignore this email. Your password will not be changed.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -78,18 +142,31 @@ export async function sendPasswordResetEmail(email: string, resetToken: string)
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background-color: #f8f8f8; padding: 30px; text-align: center; border-top: 1px solid #eeeeee;">
|
<td style="background-color: #f8f9fa; padding: 35px 40px; border-top: 1px solid #e9ecef;">
|
||||||
<p style="margin: 0 0 10px 0; color: #999999; font-size: 12px;">
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
© 2025 QR Master. All rights reserved.
|
<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>
|
||||||
<p style="margin: 0; color: #999999; font-size: 12px;">
|
<p style="margin: 0; color: #999999; font-size: 12px;">
|
||||||
This is an automated email. Please do not reply.
|
© 2025 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>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -104,77 +181,164 @@ export async function sendPasswordResetEmail(email: string, resetToken: string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Newsletter Welcome Email - Value-focused with clear expectations
|
||||||
|
*/
|
||||||
export async function sendNewsletterWelcomeEmail(email: string) {
|
export async function sendNewsletterWelcomeEmail(email: string) {
|
||||||
|
await waitForRateLimit();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await resend.emails.send({
|
await resend.emails.send({
|
||||||
from: 'QR Master <onboarding@resend.dev>',
|
from: 'Timo from QR Master <onboarding@resend.dev>',
|
||||||
replyTo: 'support@qrmaster.net',
|
replyTo: 'support@qrmaster.net',
|
||||||
to: email,
|
to: email,
|
||||||
subject: '🎉 You\'re on the list! AI-Powered QR Features Coming Soon',
|
subject: '🎉 You\'re In! Here\'s What Happens Next (AI QR Features)',
|
||||||
html: `
|
html: `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Welcome to QR Master AI Newsletter</title>
|
<title>Welcome to QR Master AI Waitlist</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
|
<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: 20px 0;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f4; padding: 40px 20px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
<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 -->
|
|
||||||
|
<!-- Hero Header -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 30px; text-align: center;">
|
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 50px 40px; text-align: center;">
|
||||||
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: bold;">QR Master</h1>
|
<div style="display: inline-block; background-color: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 50px; margin-bottom: 20px;">
|
||||||
<p style="margin: 10px 0 0 0; color: #ffffff; font-size: 16px; opacity: 0.9;">AI-Powered QR Features</p>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Personal Note -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 40px 30px;">
|
<td style="padding: 45px 40px 35px 40px;">
|
||||||
<h2 style="margin: 0 0 20px 0; color: #333333; font-size: 24px;">Welcome to the Future! 🚀</h2>
|
<p style="margin: 0 0 20px 0; color: #333333; font-size: 16px; line-height: 1.7;">
|
||||||
|
Hey there! 👋
|
||||||
<p style="margin: 0 0 20px 0; color: #666666; font-size: 16px; line-height: 1.6;">
|
|
||||||
Thank you for signing up to be notified about our revolutionary AI-powered QR code features! You're among the first to know when these game-changing capabilities launch.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border-left: 4px solid #667eea; padding: 20px; margin: 30px 0; border-radius: 4px;">
|
<p style="margin: 0 0 25px 0; color: #555555; font-size: 16px; line-height: 1.7;">
|
||||||
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 18px;">What's Coming:</h3>
|
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.
|
||||||
<ul style="margin: 0; padding-left: 20px; color: #666666; font-size: 15px; line-height: 1.8;">
|
</p>
|
||||||
<li><strong>Smart QR Generation</strong> - AI-powered content optimization & intelligent design suggestions</li>
|
|
||||||
<li><strong>Advanced Analytics</strong> - Scan predictions, anomaly detection & natural language queries</li>
|
<p style="margin: 0 0 35px 0; color: #555555; font-size: 16px; line-height: 1.7;">
|
||||||
<li><strong>Smart Content Management</strong> - AI chatbot, auto-categorization & smart bulk generation</li>
|
No fluff, no spam. Just a heads-up when these features go live.
|
||||||
<li><strong>Creative & Marketing</strong> - AI-generated designs, copy generation & campaign optimization</li>
|
</p>
|
||||||
</ul>
|
|
||||||
|
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<p style="margin: 20px 0; color: #666666; font-size: 16px; line-height: 1.6;">
|
<!-- Feature 2 -->
|
||||||
We're working hard to bring these features to life and can't wait to share them with you. We'll send you an email as soon as they're ready to use!
|
<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>
|
||||||
|
|
||||||
<p style="margin: 30px 0 0 0; color: #666666; font-size: 16px; line-height: 1.6;">
|
<p style="margin: 20px 0 0 0; color: #555555; font-size: 16px; line-height: 1.7;">
|
||||||
In the meantime, feel free to explore our existing features at <a href="https://www.qrmaster.net" style="color: #667eea; text-decoration: none;">qrmaster.net</a>
|
<strong>— Timo 👋</strong><br>
|
||||||
|
<span style="color: #888888; font-size: 14px;">Founder, QR Master</span>
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background-color: #f8f8f8; padding: 30px; text-align: center; border-top: 1px solid #eeeeee;">
|
<td style="background-color: #f8f9fa; padding: 35px 40px; border-top: 1px solid #e9ecef;">
|
||||||
<p style="margin: 0 0 10px 0; color: #999999; font-size: 12px;">
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
© 2025 QR Master. All rights reserved.
|
<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>
|
||||||
<p style="margin: 0; color: #999999; font-size: 12px;">
|
<p style="margin: 0; color: #999999; font-size: 12px;">
|
||||||
You're receiving this email because you signed up for AI feature notifications.
|
© 2025 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>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -189,84 +353,172 @@ export async function sendNewsletterWelcomeEmail(email: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Feature Launch Email - Excitement + clear CTA
|
||||||
|
*/
|
||||||
export async function sendAIFeatureLaunchEmail(email: string) {
|
export async function sendAIFeatureLaunchEmail(email: string) {
|
||||||
|
await waitForRateLimit();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await resend.emails.send({
|
await resend.emails.send({
|
||||||
from: 'QR Master <onboarding@resend.dev>',
|
from: 'Timo from QR Master <onboarding@resend.dev>',
|
||||||
replyTo: 'support@qrmaster.net',
|
replyTo: 'support@qrmaster.net',
|
||||||
to: email,
|
to: email,
|
||||||
subject: '🚀 AI-Powered Features Are Here! QR Master Gets Smarter',
|
subject: '🚀 They\'re Live! Your AI QR Features Are Ready',
|
||||||
html: `
|
html: `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>AI Features Launched!</title>
|
<title>AI Features Are Live - QR Master</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
|
<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: 20px 0;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f4; padding: 40px 20px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
<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 -->
|
|
||||||
|
<!-- Celebration Header -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 30px; text-align: center;">
|
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 25px 30px; text-align: center;">
|
||||||
<h1 style="margin: 0; color: #ffffff; font-size: 32px; font-weight: bold;">🚀 They're Here!</h1>
|
<div style="font-size: 32px; margin-bottom: 10px;">🎉</div>
|
||||||
<p style="margin: 10px 0 0 0; color: #ffffff; font-size: 18px; opacity: 0.95;">AI-Powered QR Features Are Live</p>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Main Content -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 40px 30px;">
|
<td style="padding: 50px 40px;">
|
||||||
<h2 style="margin: 0 0 20px 0; color: #333333; font-size: 24px;">The wait is over! ✨</h2>
|
|
||||||
|
|
||||||
<p style="margin: 0 0 20px 0; color: #666666; font-size: 16px; line-height: 1.6;">
|
<!-- Opening -->
|
||||||
We're excited to announce that the AI-powered features you've been waiting for are now live on QR Master! Your QR code creation just got a whole lot smarter.
|
<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>
|
||||||
|
|
||||||
<div style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border-left: 4px solid #667eea; padding: 20px; margin: 30px 0; border-radius: 4px;">
|
<p style="margin: 0 0 25px 0; color: #333333; font-size: 17px; line-height: 1.7; font-weight: 700;">
|
||||||
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 18px;">✨ What's New:</h3>
|
Well, today's that day. 🎯
|
||||||
<ul style="margin: 0; padding-left: 20px; color: #666666; font-size: 15px; line-height: 1.8;">
|
</p>
|
||||||
<li><strong>Smart QR Generation</strong> - AI optimizes your content and suggests the best designs automatically</li>
|
|
||||||
<li><strong>Advanced Analytics</strong> - Predictive insights and natural language queries for your scan data</li>
|
<p style="margin: 0 0 35px 0; color: #555555; font-size: 16px; line-height: 1.7;">
|
||||||
<li><strong>Auto-Organization</strong> - Your QR codes get categorized and tagged automatically</li>
|
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.
|
||||||
<li><strong>AI Design Studio</strong> - Generate unique, custom QR code designs with AI</li>
|
</p>
|
||||||
</ul>
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
<!-- CTA Button -->
|
<div style="background-color: #ffffff; padding: 25px; margin-bottom: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 30px 0;">
|
<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>
|
<tr>
|
||||||
<td align="center">
|
<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: 16px 40px; border-radius: 8px; font-size: 16px; font-weight: bold; display: inline-block;">
|
<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
|
✨ Try AI Features Now →
|
||||||
</a>
|
</a>
|
||||||
|
<p style="margin: 20px 0 0 0; color: #888888; font-size: 13px;">
|
||||||
|
✅ Available on all plans • ⚡ No extra setup required
|
||||||
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p style="margin: 20px 0 0 0; color: #666666; font-size: 16px; line-height: 1.6;">
|
<!-- Personal Note -->
|
||||||
Log in to your QR Master account and start exploring the new AI capabilities. We can't wait to see what you create!
|
<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>
|
||||||
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background-color: #f8f8f8; padding: 30px; text-align: center; border-top: 1px solid #eeeeee;">
|
<td style="background-color: #f8f9fa; padding: 35px 40px; border-top: 1px solid #e9ecef;">
|
||||||
<p style="margin: 0 0 10px 0; color: #999999; font-size: 12px;">
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
© 2025 QR Master. All rights reserved.
|
<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>
|
||||||
<p style="margin: 0; color: #999999; font-size: 12px;">
|
<p style="margin: 0; color: #999999; font-size: 12px;">
|
||||||
You received this email because you subscribed to AI feature notifications.
|
© 2025 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>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
// Use a placeholder during build time, real key at runtime
|
||||||
|
const stripeKey = process.env.STRIPE_SECRET_KEY || 'sk_test_placeholder_for_build';
|
||||||
|
|
||||||
|
export const stripe = new Stripe(stripeKey, {
|
||||||
|
apiVersion: '2025-10-29.clover',
|
||||||
|
typescript: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Runtime validation (will throw when actually used in production if not set)
|
||||||
|
export function validateStripeKey() {
|
||||||
if (!process.env.STRIPE_SECRET_KEY) {
|
if (!process.env.STRIPE_SECRET_KEY) {
|
||||||
throw new Error('STRIPE_SECRET_KEY is not set');
|
throw new Error('STRIPE_SECRET_KEY is not set');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
||||||
apiVersion: '2025-09-30.clover',
|
|
||||||
typescript: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const STRIPE_PLANS = {
|
export const STRIPE_PLANS = {
|
||||||
FREE: {
|
FREE: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue