diff --git a/server/email.ts b/server/email.ts new file mode 100644 index 0000000..f1ed81a --- /dev/null +++ b/server/email.ts @@ -0,0 +1,122 @@ +/** + * Email service powered by Resend. + * + * Required env vars: + * RESEND_API_KEY + * STORE_EMAIL_FROM (e.g. "orders@yourstore.com") + * STORE_NAME (e.g. "My Store") + */ + +import { Resend } from "resend"; + +let _resend: Resend | null = null; + +function getResend(): Resend { + if (!_resend) { + if (!process.env.RESEND_API_KEY) { + throw new Error("RESEND_API_KEY is not configured"); + } + _resend = new Resend(process.env.RESEND_API_KEY); + } + return _resend; +} + +const FROM = process.env.STORE_EMAIL_FROM || "noreply@yourstore.com"; +const STORE_NAME = process.env.STORE_NAME || "My Store"; + +export async function sendMagicLink(email: string, token: string, baseUrl: string) { + const link = `${baseUrl}/account/verify?token=${token}`; + try { + await getResend().emails.send({ + from: FROM, + to: email, + subject: `Sign in to ${STORE_NAME}`, + html: ` +

Click the link below to sign in to ${STORE_NAME}. This link expires in 15 minutes.

+

Sign In →

+

If you didn't request this, you can safely ignore this email.

+ `, + }); + } catch (err) { + console.error("[email] sendMagicLink failed:", err); + } +} + +export async function sendOrderConfirmation(params: { + email: string; + orderNumber: string; + total: string; + items: Array<{ title: string; variantTitle?: string; quantity: number; price: string }>; + shippingAddress?: any; +}) { + const itemsHtml = params.items + .map( + (item) => ` + ${item.title}${item.variantTitle ? ` — ${item.variantTitle}` : ""} + ${item.quantity} + $${item.price} + ` + ) + .join(""); + + const addr = params.shippingAddress; + const addrHtml = addr + ? `

Ship to:
${addr.firstName} ${addr.lastName}
${addr.address1}${addr.address2 ? `, ${addr.address2}` : ""}
${addr.city}, ${addr.state} ${addr.zip}

` + : ""; + + try { + await getResend().emails.send({ + from: FROM, + to: params.email, + subject: `Order confirmed — #${params.orderNumber}`, + html: ` +
+

Thanks for your order!

+

Order #${params.orderNumber} has been received and is being processed.

+ + + + + + + + + ${itemsHtml} + + + + + + +
ItemQtyPrice
Total$${params.total}
+ ${addrHtml} +

Thank you for shopping with ${STORE_NAME}.

+
+ `, + }); + } catch (err) { + console.error("[email] sendOrderConfirmation failed:", err); + } +} + +export async function sendShippingNotification(params: { + email: string; + orderNumber: string; + trackingNumber: string; + trackingCarrier?: string; +}) { + try { + await getResend().emails.send({ + from: FROM, + to: params.email, + subject: `Your order #${params.orderNumber} has shipped`, + html: ` +

Great news! Your ${STORE_NAME} order #${params.orderNumber} is on its way.

+

Tracking number: ${params.trackingNumber}${params.trackingCarrier ? ` (${params.trackingCarrier})` : ""}

+

Thank you for your order!

+ `, + }); + } catch (err) { + console.error("[email] sendShippingNotification failed:", err); + } +}