chore: add server/email.ts
This commit is contained in:
parent
3812bbe6ae
commit
2fb134f42b
1 changed files with 122 additions and 0 deletions
122
server/email.ts
Normal file
122
server/email.ts
Normal file
|
|
@ -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: `
|
||||||
|
<p>Click the link below to sign in to ${STORE_NAME}. This link expires in 15 minutes.</p>
|
||||||
|
<p><a href="${link}" style="font-weight:bold">Sign In →</a></p>
|
||||||
|
<p style="color:#888;font-size:12px">If you didn't request this, you can safely ignore this email.</p>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
} 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) => `<tr>
|
||||||
|
<td style="padding:8px 0;border-bottom:1px solid #f0f0f0">${item.title}${item.variantTitle ? ` — ${item.variantTitle}` : ""}</td>
|
||||||
|
<td style="padding:8px 0;border-bottom:1px solid #f0f0f0;text-align:center">${item.quantity}</td>
|
||||||
|
<td style="padding:8px 0;border-bottom:1px solid #f0f0f0;text-align:right">$${item.price}</td>
|
||||||
|
</tr>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const addr = params.shippingAddress;
|
||||||
|
const addrHtml = addr
|
||||||
|
? `<p><strong>Ship to:</strong><br>${addr.firstName} ${addr.lastName}<br>${addr.address1}${addr.address2 ? `, ${addr.address2}` : ""}<br>${addr.city}, ${addr.state} ${addr.zip}</p>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getResend().emails.send({
|
||||||
|
from: FROM,
|
||||||
|
to: params.email,
|
||||||
|
subject: `Order confirmed — #${params.orderNumber}`,
|
||||||
|
html: `
|
||||||
|
<div style="font-family:sans-serif;max-width:560px;margin:auto">
|
||||||
|
<h2 style="color:#333">Thanks for your order!</h2>
|
||||||
|
<p>Order <strong>#${params.orderNumber}</strong> has been received and is being processed.</p>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="text-align:left;padding-bottom:8px;border-bottom:2px solid #eee">Item</th>
|
||||||
|
<th style="text-align:center;padding-bottom:8px;border-bottom:2px solid #eee">Qty</th>
|
||||||
|
<th style="text-align:right;padding-bottom:8px;border-bottom:2px solid #eee">Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${itemsHtml}</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="padding-top:12px;text-align:right;font-weight:bold">Total</td>
|
||||||
|
<td style="padding-top:12px;text-align:right;font-weight:bold">$${params.total}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
${addrHtml}
|
||||||
|
<p style="color:#888;font-size:12px;margin-top:32px">Thank you for shopping with ${STORE_NAME}.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
} 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: `
|
||||||
|
<p>Great news! Your ${STORE_NAME} order <strong>#${params.orderNumber}</strong> is on its way.</p>
|
||||||
|
<p><strong>Tracking number:</strong> ${params.trackingNumber}${params.trackingCarrier ? ` (${params.trackingCarrier})` : ""}</p>
|
||||||
|
<p style="color:#888;font-size:12px">Thank you for your order!</p>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[email] sendShippingNotification failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue