refactor: extract billing HTML templates into billing-templates.ts (TDD)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m0s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m0s
- Extract renderSuccessPage() and renderAlreadyProvisionedPage() from billing.ts - Share common styles via SHARED_STYLES constant - 11 TDD tests: content rendering, XSS escaping, structure validation - billing.ts: 369 → 334 lines (-35 lines, inline HTML removed) - 647 tests passing (59 files), 0 tsc errors
This commit is contained in:
parent
25cb5e2e94
commit
b491052f69
3 changed files with 108 additions and 38 deletions
64
src/__tests__/billing-templates.test.ts
Normal file
64
src/__tests__/billing-templates.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { renderSuccessPage, renderAlreadyProvisionedPage } from "../utils/billing-templates.js";
|
||||||
|
|
||||||
|
describe("billing-templates", () => {
|
||||||
|
describe("renderSuccessPage", () => {
|
||||||
|
it("includes the API key in the output", () => {
|
||||||
|
const html = renderSuccessPage("df_pro_abc123");
|
||||||
|
expect(html).toContain("df_pro_abc123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes HTML in the API key", () => {
|
||||||
|
const html = renderSuccessPage('<script>alert("xss")</script>');
|
||||||
|
expect(html).not.toContain("<script>");
|
||||||
|
expect(html).toContain("<script>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes Welcome to Pro heading", () => {
|
||||||
|
const html = renderSuccessPage("df_pro_test");
|
||||||
|
expect(html).toContain("Welcome to Pro");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes copy button with data-copy attribute", () => {
|
||||||
|
const html = renderSuccessPage("df_pro_key123");
|
||||||
|
expect(html).toContain('data-copy="df_pro_key123"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes copy-helper.js script", () => {
|
||||||
|
const html = renderSuccessPage("df_pro_test");
|
||||||
|
expect(html).toContain("copy-helper.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes docs link", () => {
|
||||||
|
const html = renderSuccessPage("df_pro_test");
|
||||||
|
expect(html).toContain("/docs");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts with DOCTYPE", () => {
|
||||||
|
const html = renderSuccessPage("df_pro_test");
|
||||||
|
expect(html.trimStart()).toMatch(/^<!DOCTYPE html>/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("renderAlreadyProvisionedPage", () => {
|
||||||
|
it("indicates key already provisioned", () => {
|
||||||
|
const html = renderAlreadyProvisionedPage();
|
||||||
|
expect(html).toContain("Already Provisioned");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mentions key recovery", () => {
|
||||||
|
const html = renderAlreadyProvisionedPage();
|
||||||
|
expect(html).toContain("recovery");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes docs link", () => {
|
||||||
|
const html = renderAlreadyProvisionedPage();
|
||||||
|
expect(html).toContain("/docs");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts with DOCTYPE", () => {
|
||||||
|
const html = renderAlreadyProvisionedPage();
|
||||||
|
expect(html.trimStart()).toMatch(/^<!DOCTYPE html>/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,7 +4,7 @@ import Stripe from "stripe";
|
||||||
import { createProKey, downgradeByCustomer, updateEmailByCustomer, findKeyByCustomerId } from "../services/keys.js";
|
import { createProKey, downgradeByCustomer, updateEmailByCustomer, findKeyByCustomerId } from "../services/keys.js";
|
||||||
import logger from "../services/logger.js";
|
import logger from "../services/logger.js";
|
||||||
|
|
||||||
import { escapeHtml } from "../utils/html.js";
|
import { renderSuccessPage, renderAlreadyProvisionedPage } from "../utils/billing-templates.js";
|
||||||
|
|
||||||
let _stripe: Stripe | null = null;
|
let _stripe: Stripe | null = null;
|
||||||
function getStripe(): Stripe {
|
function getStripe(): Stripe {
|
||||||
|
|
@ -171,49 +171,14 @@ router.get("/success", async (req: Request, res: Response) => {
|
||||||
const existingKey = await findKeyByCustomerId(customerId);
|
const existingKey = await findKeyByCustomerId(customerId);
|
||||||
if (existingKey) {
|
if (existingKey) {
|
||||||
provisionedSessions.set(session.id, Date.now());
|
provisionedSessions.set(session.id, Date.now());
|
||||||
res.send(`<!DOCTYPE html>
|
res.send(renderAlreadyProvisionedPage());
|
||||||
<html><head><title>DocFast Pro — Key Already Provisioned</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: system-ui; background: #0a0a0a; color: #e8e8e8; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
||||||
.card { background: #141414; border: 1px solid #222; border-radius: 16px; padding: 48px; max-width: 500px; text-align: center; }
|
|
||||||
h1 { color: #4f9; margin-bottom: 8px; }
|
|
||||||
p { color: #888; line-height: 1.6; }
|
|
||||||
a { color: #4f9; }
|
|
||||||
</style></head><body>
|
|
||||||
<div class="card">
|
|
||||||
<h1>✅ Key Already Provisioned</h1>
|
|
||||||
<p>A Pro API key has already been created for this purchase.</p>
|
|
||||||
<p>If you lost your key, use the <a href="/docs#key-recovery">key recovery feature</a>.</p>
|
|
||||||
<p><a href="/docs">View API docs →</a></p>
|
|
||||||
</div></body></html>`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyInfo = await createProKey(email, customerId);
|
const keyInfo = await createProKey(email, customerId);
|
||||||
provisionedSessions.set(session.id, Date.now());
|
provisionedSessions.set(session.id, Date.now());
|
||||||
|
|
||||||
// Return a nice HTML page instead of raw JSON
|
res.send(renderSuccessPage(keyInfo.key));
|
||||||
res.send(`<!DOCTYPE html>
|
|
||||||
<html><head><title>Welcome to DocFast Pro!</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: system-ui; background: #0a0a0a; color: #e8e8e8; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
||||||
.card { background: #141414; border: 1px solid #222; border-radius: 16px; padding: 48px; max-width: 500px; text-align: center; }
|
|
||||||
h1 { color: #4f9; margin-bottom: 8px; }
|
|
||||||
.key { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 16px; margin: 24px 0; font-family: monospace; font-size: 0.9rem; word-break: break-all; cursor: pointer; }
|
|
||||||
.key:hover { border-color: #4f9; }
|
|
||||||
p { color: #888; line-height: 1.6; }
|
|
||||||
a { color: #4f9; }
|
|
||||||
</style></head><body>
|
|
||||||
<div class="card">
|
|
||||||
<h1>🎉 Welcome to Pro!</h1>
|
|
||||||
<p>Your API key:</p>
|
|
||||||
<div class="key" style="position:relative">${escapeHtml(keyInfo.key)}<button data-copy="${escapeHtml(keyInfo.key)}" style="position:absolute;top:8px;right:8px;background:#4f9;color:#0a0a0a;border:none;border-radius:4px;padding:4px 12px;cursor:pointer;font-size:0.8rem;font-family:system-ui">Copy</button></div>
|
|
||||||
<p><strong>Save this key!</strong> It won't be shown again.</p>
|
|
||||||
<p>5,000 PDFs/month • All endpoints • Priority support</p>
|
|
||||||
<p><a href="/docs">View API docs →</a></p>
|
|
||||||
</div>
|
|
||||||
<script src="/copy-helper.js"></script>
|
|
||||||
</body></html>`);
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
logger.error({ err }, "Success page error");
|
logger.error({ err }, "Success page error");
|
||||||
res.status(500).json({ error: "Failed to retrieve session" });
|
res.status(500).json({ error: "Failed to retrieve session" });
|
||||||
|
|
|
||||||
41
src/utils/billing-templates.ts
Normal file
41
src/utils/billing-templates.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { escapeHtml } from "./html.js";
|
||||||
|
|
||||||
|
const SHARED_STYLES = `
|
||||||
|
body { font-family: system-ui; background: #0a0a0a; color: #e8e8e8; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
||||||
|
.card { background: #141414; border: 1px solid #222; border-radius: 16px; padding: 48px; max-width: 500px; text-align: center; }
|
||||||
|
h1 { color: #4f9; margin-bottom: 8px; }
|
||||||
|
p { color: #888; line-height: 1.6; }
|
||||||
|
a { color: #4f9; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function renderSuccessPage(apiKey: string): string {
|
||||||
|
const escaped = escapeHtml(apiKey);
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html><head><title>Welcome to DocFast Pro!</title>
|
||||||
|
<style>${SHARED_STYLES}
|
||||||
|
.key { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 16px; margin: 24px 0; font-family: monospace; font-size: 0.9rem; word-break: break-all; cursor: pointer; }
|
||||||
|
.key:hover { border-color: #4f9; }
|
||||||
|
</style></head><body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>🎉 Welcome to Pro!</h1>
|
||||||
|
<p>Your API key:</p>
|
||||||
|
<div class="key" style="position:relative">${escaped}<button data-copy="${escaped}" style="position:absolute;top:8px;right:8px;background:#4f9;color:#0a0a0a;border:none;border-radius:4px;padding:4px 12px;cursor:pointer;font-size:0.8rem;font-family:system-ui">Copy</button></div>
|
||||||
|
<p><strong>Save this key!</strong> It won't be shown again.</p>
|
||||||
|
<p>5,000 PDFs/month • All endpoints • Priority support</p>
|
||||||
|
<p><a href="/docs">View API docs →</a></p>
|
||||||
|
</div>
|
||||||
|
<script src="/copy-helper.js"></script>
|
||||||
|
</body></html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderAlreadyProvisionedPage(): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html><head><title>DocFast Pro — Key Already Provisioned</title>
|
||||||
|
<style>${SHARED_STYLES}</style></head><body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>✅ Key Already Provisioned</h1>
|
||||||
|
<p>A Pro API key has already been created for this purchase.</p>
|
||||||
|
<p>If you lost your key, use the <a href="/docs#key-recovery">key recovery feature</a>.</p>
|
||||||
|
<p><a href="/docs">View API docs →</a></p>
|
||||||
|
</div></body></html>`;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue