feat: Add Saleor webhook handler with Resend email integration

- Add Resend SDK for transactional emails
- Create React Email templates for order events:
  - OrderConfirmation
  - OrderShipped
  - OrderCancelled
  - OrderPaid
- Multi-language support (SR, EN, DE, FR)
- Customer emails in their language
- Admin emails in English to me@hytham.me and tamara@hytham.me
- Webhook handler at /api/webhooks/saleor
- Supports: ORDER_CONFIRMED, ORDER_FULLY_PAID, ORDER_CANCELLED, ORDER_FULFILLED
- Add GraphQL mutation to create webhooks in Saleor
- Add Resend API key to .env.local
This commit is contained in:
Unchained
2026-03-25 10:10:57 +02:00
parent 00f63c32f8
commit b8b3a57e6f
11 changed files with 2249 additions and 0 deletions

100
src/lib/resend.ts Normal file
View File

@@ -0,0 +1,100 @@
import { Resend } from "resend";
let resendClient: Resend | null = null;
function getResendClient(): Resend {
if (!resendClient) {
if (!process.env.RESEND_API_KEY) {
throw new Error("RESEND_API_KEY environment variable is not set");
}
resendClient = new Resend(process.env.RESEND_API_KEY);
}
return resendClient;
}
export const ADMIN_EMAILS = ["me@hytham.me", "tamara@hytham.me"];
export async function sendEmail({
to,
subject,
react,
text,
tags,
idempotencyKey,
}: {
to: string | string[];
subject: string;
react: React.ReactNode;
text?: string;
tags?: { name: string; value: string }[];
idempotencyKey?: string;
}) {
const resend = getResendClient();
const { data, error } = await resend.emails.send(
{
from: "ManoonOils <support@manoonoils.com>",
to: Array.isArray(to) ? to : [to],
subject,
react,
text,
tags,
...(idempotencyKey && { idempotencyKey }),
}
);
if (error) {
console.error("Failed to send email:", error);
throw error;
}
return data;
}
export async function sendEmailToCustomer({
to,
subject,
react,
text,
language,
idempotencyKey,
}: {
to: string;
subject: string;
react: React.ReactNode;
text?: string;
language: string;
idempotencyKey?: string;
}) {
const tag = `customer-${language}`;
return sendEmail({
to,
subject,
react,
text,
tags: [{ name: "type", value: tag }],
idempotencyKey,
});
}
export async function sendEmailToAdmin({
subject,
react,
text,
eventType,
orderId,
}: {
subject: string;
react: React.ReactNode;
text?: string;
eventType: string;
orderId: string;
}) {
return sendEmail({
to: ADMIN_EMAILS,
subject: `[Admin] ${subject}`,
react,
text,
tags: [{ name: "type", value: "admin-notification" }],
idempotencyKey: `admin-${eventType}/${orderId}`,
});
}

View File

@@ -0,0 +1,77 @@
mutation CreateSaleorWebhooks {
orderConfirmedWebhook: webhookCreate(input: {
name: "Resend - Order Confirmed"
targetUrl: "https://manoonoils.com/api/webhooks/saleor"
events: [ORDER_CONFIRMED]
isActive: true
}) {
webhook {
id
name
targetUrl
isActive
}
errors {
field
message
code
}
}
orderPaidWebhook: webhookCreate(input: {
name: "Resend - Order Paid"
targetUrl: "https://manoonoils.com/api/webhooks/saleor"
events: [ORDER_FULLY_PAID]
isActive: true
}) {
webhook {
id
name
targetUrl
isActive
}
errors {
field
message
code
}
}
orderCancelledWebhook: webhookCreate(input: {
name: "Resend - Order Cancelled"
targetUrl: "https://manoonoils.com/api/webhooks/saleor"
events: [ORDER_CANCELLED]
isActive: true
}) {
webhook {
id
name
targetUrl
isActive
}
errors {
field
message
code
}
}
orderFulfilledWebhook: webhookCreate(input: {
name: "Resend - Order Fulfilled"
targetUrl: "https://manoonoils.com/api/webhooks/saleor"
events: [ORDER_FULFILLED]
isActive: true
}) {
webhook {
id
name
targetUrl
isActive
}
errors {
field
message
code
}
}
}