| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { v } from "convex/values"; |
| import { internalAction } from "../_generated/server"; |
| import { |
| PRO_LAUNCH_FROM, |
| PRO_LAUNCH_HTML, |
| PRO_LAUNCH_PHYSICAL_ADDRESS, |
| PRO_LAUNCH_REPLY_TO, |
| PRO_LAUNCH_SUBJECT, |
| PRO_LAUNCH_TEXT, |
| } from "./proLaunchEmailContent"; |
|
|
| const RESEND_API_BASE = "https://api.resend.com"; |
| const USER_AGENT = "WorldMonitor-PROLaunchSender/1.0 (+https://worldmonitor.app)"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const createProLaunchBroadcast = internalAction({ |
| args: { |
| segmentId: v.string(), |
| nameSuffix: v.optional(v.string()), |
| }, |
| handler: async (_ctx, { segmentId, nameSuffix }) => { |
| const apiKey = process.env.RESEND_API_KEY; |
| if (!apiKey) { |
| throw new Error("[createProLaunchBroadcast] RESEND_API_KEY not set"); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| if ( |
| PRO_LAUNCH_PHYSICAL_ADDRESS.includes("TBD") || |
| PRO_LAUNCH_PHYSICAL_ADDRESS.includes("placeholder") |
| ) { |
| throw new Error( |
| `[createProLaunchBroadcast] PRO_LAUNCH_PHYSICAL_ADDRESS is still a placeholder ("${PRO_LAUNCH_PHYSICAL_ADDRESS}"). ` + |
| "Set the real CAN-SPAM postal address in convex/broadcast/proLaunchEmailContent.ts before sending.", |
| ); |
| } |
|
|
| const name = |
| nameSuffix && nameSuffix.length > 0 |
| ? `PRO Launch — ${nameSuffix}` |
| : `PRO Launch — ${new Date().toISOString().slice(0, 10)}`; |
|
|
| const res = await fetch(`${RESEND_API_BASE}/broadcasts`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| Authorization: `Bearer ${apiKey}`, |
| "User-Agent": USER_AGENT, |
| }, |
| body: JSON.stringify({ |
| name, |
| segment_id: segmentId, |
| from: PRO_LAUNCH_FROM, |
| reply_to: PRO_LAUNCH_REPLY_TO, |
| subject: PRO_LAUNCH_SUBJECT, |
| html: PRO_LAUNCH_HTML, |
| text: PRO_LAUNCH_TEXT, |
| }), |
| }); |
|
|
| if (!res.ok) { |
| const body = await res.text().catch(() => "<no body>"); |
| throw new Error( |
| `[createProLaunchBroadcast] Resend ${res.status}: ${body}`, |
| ); |
| } |
|
|
| const json = (await res.json().catch(() => null)) as { |
| id?: string; |
| } | null; |
| if (!json?.id) { |
| throw new Error( |
| `[createProLaunchBroadcast] Resend response missing id: ${JSON.stringify(json)}`, |
| ); |
| } |
|
|
| return { |
| broadcastId: json.id, |
| name, |
| segmentId, |
| subject: PRO_LAUNCH_SUBJECT, |
| }; |
| }, |
| }); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const sendProLaunchBroadcast = internalAction({ |
| args: { |
| broadcastId: v.string(), |
| scheduledAt: v.optional(v.string()), |
| }, |
| handler: async (_ctx, { broadcastId, scheduledAt }) => { |
| const apiKey = process.env.RESEND_API_KEY; |
| if (!apiKey) { |
| throw new Error("[sendProLaunchBroadcast] RESEND_API_KEY not set"); |
| } |
|
|
| const body: Record<string, unknown> = {}; |
| if (scheduledAt) body.scheduled_at = scheduledAt; |
|
|
| const res = await fetch( |
| `${RESEND_API_BASE}/broadcasts/${encodeURIComponent(broadcastId)}/send`, |
| { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| Authorization: `Bearer ${apiKey}`, |
| "User-Agent": USER_AGENT, |
| }, |
| body: JSON.stringify(body), |
| }, |
| ); |
|
|
| if (!res.ok) { |
| const errBody = await res.text().catch(() => "<no body>"); |
| throw new Error( |
| `[sendProLaunchBroadcast] Resend ${res.status}: ${errBody}`, |
| ); |
| } |
|
|
| return { |
| broadcastId, |
| status: scheduledAt ? "scheduled" : "queued", |
| scheduledAt: scheduledAt ?? null, |
| }; |
| }, |
| }); |
|
|