Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/waitlist/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function POST(req: NextRequest) {
// Double opt-in: e-mail a confirmation link via Resend. If Resend isn't
// configured (local dev / self-host), auto-confirm so the flow still works.
if (result.token && isEmailConfigured()) {
const sent = await sendWaitlistVerification({ email, token: result.token, brand });
const sent = await sendWaitlistVerification({ email, token: result.token, brand, site: domain });
if (!sent.ok) {
// Never block a signup because email is misconfigured (bad key, unverified
// domain, Resend down). Save them and auto-confirm; log loudly so the
Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// RESEND_API_KEY is unset — e.g. local dev — isEmailConfigured() is false and
// callers fall back to auto-confirming so the flow still works offline.

import { promoBlock } from "@profullstack/cross-promo";

const RESEND_ENDPOINT = "https://api.resend.com/emails";

/**
Expand Down Expand Up @@ -87,18 +89,31 @@ async function send(opts: {
}

/** Sends the "confirm your spot" email with a one-click verification link. */
export function sendWaitlistVerification(opts: {
export async function sendWaitlistVerification(opts: {
email: string;
token: string;
brand?: string;
/** The tenant domain the signup came from (?dn=); defaults to moshcoding.com. */
site?: string;
}): Promise<SendResult> {
const brand = opts.brand || "moshcoding";
// "More from the network" for developers, below the confirm button so it
// never competes with it. Never the sending site or moshcoding itself.
// Never throws; falls back to the bundled catalog if the CDN is down.
const promo = await promoBlock({
site: opts.site || "moshcoding.com",
exclude: ["moshcoding.com"],
audience: "developers",
seed: opts.email,
theme: { accent: "#9EF01A", text: "#c9c9c9", muted: "#8a8a8a", bg: "#141416", border: "#26262a", heading: "#e7e7e7" },
});
const url = `${appBaseUrl()}/api/waitlist/verify?token=${encodeURIComponent(opts.token)}`;
const subject = `Confirm your spot in the ${brand} pit 🤘`;
const text =
`You're almost in. Confirm your email to lock in your spot on the ${brand} waitlist:\n\n` +
`${url}\n\n` +
`If you didn't request this, just ignore it — no account is created.`;
`If you didn't request this, just ignore it — no account is created.` +
(promo.text ? `\n\n${promo.text}` : "");
const safeBrand = escapeHtml(brand);
const safeUrl = escapeHtml(url);
const html = `<!doctype html>
Expand All @@ -124,6 +139,9 @@ export function sendWaitlistVerification(opts: {
</p>
</td></tr>
</table>
<table role="presentation" width="100%" style="max-width:480px">
<tr><td>${promo.html}</td></tr>
</table>
</td></tr>
</table>
</body></html>`;
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "moshcoding",
"version": "0.4.1",
"version": "0.5.0",
"private": true,
"description": "Code hard. Mosh harder. Next.js + Bun. Landing + one-line metal coming-soon pages with email waitlist and CoinPayPortal login.",
"scripts": {
Expand All @@ -14,6 +14,7 @@
"dependencies": {
"@profullstack/affiliates": "0.3.0",
"@profullstack/coinpay-onboard": "^0.5.0",
"@profullstack/cross-promo": "^0.1.0",
"@profullstack/footer": "latest",
"@profullstack/libsql-pg": "^0.1.4",
"@profullstack/player": "0.17.0",
Expand Down
53 changes: 53 additions & 0 deletions tests/waitlist-promo.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import test from "node:test";

import { sendWaitlistVerification } from "../lib/email.ts";

// The waitlist confirm email carries the shared "more from the network" block
// (@profullstack/cross-promo) below the confirm button, never promoting the
// sender. Capture the outgoing Resend payload; use the bundled catalog.
async function capture(opts) {
const prev = { key: process.env.RESEND_API_KEY, promo: process.env.CROSS_PROMO, fetch: globalThis.fetch };
process.env.RESEND_API_KEY = "test_key";
process.env.CROSS_PROMO = "bundled";
let captured = null;
globalThis.fetch = async (_url, init) => {
captured = JSON.parse(init.body);
return { ok: true, json: async () => ({ id: "x" }), text: async () => "" };
};
try {
await sendWaitlistVerification(opts);
} finally {
globalThis.fetch = prev.fetch;
for (const [k, v] of [["RESEND_API_KEY", prev.key], ["CROSS_PROMO", prev.promo]]) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
}
return captured;
}

const promoLinks = (html) =>
[...html.matchAll(/href="(https?:\/\/[^"]*utm_campaign=cross-promo[^"]*)"/g)].map((m) => new URL(m[1].replace(/&amp;/g, "&")));

test("waitlist email shows the network block below the confirm button", async () => {
const p = await capture({ email: "u@example.com", token: "tok" });
const links = promoLinks(p.html);
assert.ok(links.length > 0, "html carries the promo block");
assert.ok(p.html.indexOf("utm_campaign=cross-promo") > p.html.indexOf("Confirm my spot"), "below the confirm button");
assert.ok(p.html.indexOf("utm_campaign=cross-promo") > p.html.indexOf("/api/waitlist/verify?token=tok"), "below the confirm link");
assert.match(p.text, /utm_campaign=cross-promo/);
assert.ok(p.text.indexOf("utm_campaign") > p.text.indexOf("/api/waitlist/verify"), "text: confirm link first");
for (const u of links) {
assert.notEqual(u.hostname, "moshcoding.com", "never promotes moshcoding.com");
assert.equal(u.searchParams.get("utm_source"), "moshcoding.com");
}
});

test("a tenant on a catalog property never promotes itself", async () => {
const p = await capture({ email: "u@example.com", token: "tok", brand: "hqterm", site: "hqterm.sh" });
const links = promoLinks(p.html);
assert.ok(links.length > 0);
for (const u of links) assert.ok(!["hqterm.sh", "moshcoding.com"].includes(u.hostname));
assert.doesNotMatch(p.text, /https:\/\/hqterm\.sh/);
});
Loading