Skip to content

Commit efc5d95

Browse files
ralyodioclaude
andcommitted
feat: brand purchase-receipt email + extract shared shell
Pulled the dark/mint card layout from the audit-ready email into a private emailShell() helper so the receipt email (also a PDF-bearing transactional message) renders with the same look as the audit-ready mail and the supabase auth templates. The receipt now shows a labeled detail grid (pack, credits, total, order, optional tx hash), a 'View billing' CTA, and a footer note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9bbf89c commit efc5d95

1 file changed

Lines changed: 118 additions & 40 deletions

File tree

‎lib/email.ts‎

Lines changed: 118 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,24 @@ function client() {
1010
}
1111

1212
// Shared CrawlProof email shell — mirrors supabase/templates/* so transactional
13-
// mail (audit-ready, receipts) looks the same as auth mail. Score colors hint
14-
// at how the user should feel about the result.
13+
// mail (audit-ready, receipts) looks the same as auth mail.
1514
function scoreAccent(score: number): { bg: string; fg: string } {
1615
if (score >= 80) return { bg: "#6ee7b7", fg: "#042f1a" };
1716
if (score >= 60) return { bg: "#fcd34d", fg: "#3a2a05" };
1817
return { bg: "#fca5a5", fg: "#3a0808" };
1918
}
2019

21-
export function auditReadyEmailHtml(input: {
22-
targetUrl: string;
23-
score: number;
24-
reportUrl: string;
25-
pdfAttached?: boolean;
20+
function emailShell(input: {
21+
title: string;
22+
innerHtml: string;
23+
footerNote?: string;
2624
}): string {
27-
const host = (() => {
28-
try { return new URL(input.targetUrl).hostname; } catch { return input.targetUrl; }
29-
})();
30-
const accent = scoreAccent(input.score);
31-
const attachedLine = input.pdfAttached
32-
? `<p style="margin:14px 0 0;font-size:13px;line-height:1.6;color:#64748b;">A PDF copy of this report is attached to this email.</p>`
33-
: "";
3425
return `<!doctype html>
3526
<html lang="en">
3627
<head>
3728
<meta charset="utf-8">
3829
<meta name="viewport" content="width=device-width,initial-scale=1">
39-
<title>Your CrawlProof audit for ${host} is ready</title>
30+
<title>${input.title}</title>
4031
</head>
4132
<body style="margin:0;padding:0;background:#0b0d10;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#e7e9ee;">
4233
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#0b0d10;padding:32px 16px;">
@@ -55,7 +46,38 @@ export function auditReadyEmailHtml(input: {
5546
</table>
5647
</td>
5748
</tr>
58-
<tr>
49+
${input.innerHtml}
50+
${input.footerNote ? `<tr>
51+
<td style="padding:16px 32px 28px;border-top:1px solid #1f2630;">
52+
<p style="margin:0;font-size:12px;line-height:1.6;color:#64748b;">${input.footerNote}</p>
53+
</td>
54+
</tr>` : ""}
55+
</table>
56+
<p style="margin:18px 0 0;font-size:11px;color:#475569;">
57+
CrawlProof · See your site the way AI crawlers do.<br>
58+
<a href="${env.siteUrl}" style="color:#64748b;">${env.siteUrl}</a>
59+
</p>
60+
</td>
61+
</tr>
62+
</table>
63+
</body>
64+
</html>`;
65+
}
66+
67+
export function auditReadyEmailHtml(input: {
68+
targetUrl: string;
69+
score: number;
70+
reportUrl: string;
71+
pdfAttached?: boolean;
72+
}): string {
73+
const host = (() => {
74+
try { return new URL(input.targetUrl).hostname; } catch { return input.targetUrl; }
75+
})();
76+
const accent = scoreAccent(input.score);
77+
const attachedLine = input.pdfAttached
78+
? `<p style="margin:14px 0 0;font-size:13px;line-height:1.6;color:#64748b;">A PDF copy of this report is attached to this email.</p>`
79+
: "";
80+
const innerHtml = `<tr>
5981
<td style="padding:24px 32px 0;">
6082
<h1 style="margin:0;font-size:22px;line-height:1.3;font-weight:800;color:#e7e9ee;">
6183
Your audit for ${host} is ready
@@ -93,25 +115,82 @@ export function auditReadyEmailHtml(input: {
93115
</p>
94116
${attachedLine}
95117
</td>
118+
</tr>`;
119+
return emailShell({
120+
title: `Your CrawlProof audit for ${host} is ready`,
121+
innerHtml,
122+
footerNote: "Want to track this site over time? Sign in and add it as a project to schedule weekly or monthly re-audits.",
123+
});
124+
}
125+
126+
export function purchaseReceiptEmailHtml(input: {
127+
packLabel: string;
128+
creditsAdded: number;
129+
amountFormatted: string;
130+
currency: string;
131+
paymentId: string;
132+
txHash?: string | null;
133+
}): string {
134+
const txRow = input.txHash
135+
? `<tr>
136+
<td style="padding:6px 0;color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.1em;">Tx</td>
137+
<td style="padding:6px 0;color:#e7e9ee;font-family:'JetBrains Mono',ui-monospace,monospace;font-size:12px;word-break:break-all;">${input.txHash}</td>
138+
</tr>`
139+
: "";
140+
const innerHtml = `<tr>
141+
<td style="padding:24px 32px 0;">
142+
<h1 style="margin:0;font-size:22px;line-height:1.3;font-weight:800;color:#e7e9ee;">
143+
Thanks for your purchase
144+
</h1>
145+
<p style="margin:12px 0 0;font-size:14px;line-height:1.6;color:#9aa3b2;">
146+
<strong style="color:#6ee7b7;">${input.creditsAdded} credit${input.creditsAdded === 1 ? "" : "s"}</strong>
147+
added to your CrawlProof account.
148+
</p>
149+
</td>
96150
</tr>
97151
<tr>
98-
<td style="padding:16px 32px 28px;border-top:1px solid #1f2630;">
99-
<p style="margin:0;font-size:12px;line-height:1.6;color:#64748b;">
100-
Want to track this site over time? Sign in and add it as a
101-
project to schedule weekly or monthly re-audits.
102-
</p>
152+
<td style="padding:20px 32px 0;">
153+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#0b0d10;border:1px solid #1f2630;border-radius:10px;padding:14px 18px;">
154+
<tr>
155+
<td style="padding:6px 0;color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.1em;">Pack</td>
156+
<td style="padding:6px 0;color:#e7e9ee;font-size:14px;font-weight:600;text-align:right;">${input.packLabel}</td>
157+
</tr>
158+
<tr>
159+
<td style="padding:6px 0;color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.1em;">Credits</td>
160+
<td style="padding:6px 0;color:#e7e9ee;font-size:14px;font-weight:600;text-align:right;">${input.creditsAdded}</td>
161+
</tr>
162+
<tr>
163+
<td style="padding:6px 0;color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.1em;">Total</td>
164+
<td style="padding:6px 0;color:#e7e9ee;font-size:14px;font-weight:600;text-align:right;">${input.amountFormatted} ${input.currency}</td>
165+
</tr>
166+
<tr>
167+
<td style="padding:6px 0;color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.1em;">Order</td>
168+
<td style="padding:6px 0;color:#e7e9ee;font-family:'JetBrains Mono',ui-monospace,monospace;font-size:12px;text-align:right;word-break:break-all;">${input.paymentId}</td>
169+
</tr>
170+
${txRow}
171+
</table>
103172
</td>
104173
</tr>
105-
</table>
106-
<p style="margin:18px 0 0;font-size:11px;color:#475569;">
107-
CrawlProof · See your site the way AI crawlers do.<br>
108-
<a href="${env.siteUrl}" style="color:#64748b;">${env.siteUrl}</a>
109-
</p>
110-
</td>
111-
</tr>
112-
</table>
113-
</body>
114-
</html>`;
174+
<tr>
175+
<td style="padding:22px 32px 8px;">
176+
<a href="${env.siteUrl}/settings/billing"
177+
style="display:inline-block;padding:12px 22px;background:#6ee7b7;color:#042f1a;text-decoration:none;border-radius:10px;font-weight:700;font-size:15px;">
178+
View billing →
179+
</a>
180+
</td>
181+
</tr>
182+
<tr>
183+
<td style="padding:8px 32px 24px;">
184+
<p style="margin:8px 0 0;font-size:13px;line-height:1.6;color:#64748b;">
185+
A PDF copy of this receipt is attached for your records.
186+
</p>
187+
</td>
188+
</tr>`;
189+
return emailShell({
190+
title: `Your CrawlProof receipt`,
191+
innerHtml,
192+
footerNote: "Credits never expire and can be used on any scan engine.",
193+
});
115194
}
116195

117196
export async function sendAuditReadyEmail(input: {
@@ -201,20 +280,19 @@ export async function sendPurchaseReceiptEmail(input: {
201280
minimumFractionDigits: input.amountCents % 100 === 0 ? 0 : 2,
202281
})}`;
203282
const filename = `crawlproof-receipt-${input.paymentId.slice(0, 12)}.pdf`;
204-
const txLine = input.txHash
205-
? `<p style="color:#666;font-size:12px"><strong>Tx:</strong> <code>${input.txHash}</code></p>`
206-
: "";
207283

208284
const res = await c.emails.send({
209285
from: env.resendFrom,
210286
to: input.to,
211287
subject: `Your CrawlProof receipt — ${input.creditsAdded} credit${input.creditsAdded === 1 ? "" : "s"}`,
212-
html: `<p>Thanks for your purchase!</p>
213-
<p><strong>${input.packLabel}</strong> — ${input.creditsAdded} credit${input.creditsAdded === 1 ? "" : "s"} added to your account.</p>
214-
<p><strong>Total paid:</strong> ${amount} ${input.currency}</p>
215-
<p><strong>Order:</strong> ${input.paymentId}</p>
216-
${txLine}
217-
<p>A PDF copy of this receipt is attached.</p>`,
288+
html: purchaseReceiptEmailHtml({
289+
packLabel: input.packLabel,
290+
creditsAdded: input.creditsAdded,
291+
amountFormatted: amount,
292+
currency: input.currency,
293+
paymentId: input.paymentId,
294+
txHash: input.txHash,
295+
}),
218296
attachments: [{ filename, content: input.pdf.toString("base64") }],
219297
});
220298
if (res.error) return { sent: false, error: String(res.error) };

0 commit comments

Comments
 (0)