feat(emails): implement transactional email system with Resend
Some checks failed
Build and Deploy / build (push) Has been cancelled

- Add Resend email integration with @react-email/render
- Create email templates: OrderConfirmation, OrderShipped, OrderCancelled, OrderPaid
- Implement webhook handler for ORDER_CREATED and other events
- Add multi-language support for customer emails
- Admin emails in English with order details
- Update checkout page with auto-scroll on order completion
- Configure DASHBOARD_URL environment variable
This commit is contained in:
Unchained
2026-03-25 14:13:34 +02:00
parent ef83538d0b
commit 5576946829
12 changed files with 446 additions and 96 deletions

35
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@apollo/client": "^4.1.6", "@apollo/client": "^4.1.6",
"@react-email/components": "^1.0.10", "@react-email/components": "^1.0.10",
"@react-email/render": "^2.0.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.34.4", "framer-motion": "^12.34.4",
"graphql": "^16.13.1", "graphql": "^16.13.1",
@@ -1743,23 +1744,6 @@
"react": "^18.0 || ^19.0 || ^19.0.0-rc" "react": "^18.0 || ^19.0 || ^19.0.0-rc"
} }
}, },
"node_modules/@react-email/components/node_modules/@react-email/render": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.4.tgz",
"integrity": "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g==",
"license": "MIT",
"dependencies": {
"html-to-text": "^9.0.5",
"prettier": "^3.5.3"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
"node_modules/@react-email/container": { "node_modules/@react-email/container": {
"version": "0.0.16", "version": "0.0.16",
"resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.16.tgz", "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.16.tgz",
@@ -1883,6 +1867,23 @@
"react": "^18.0 || ^19.0 || ^19.0.0-rc" "react": "^18.0 || ^19.0 || ^19.0.0-rc"
} }
}, },
"node_modules/@react-email/render": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.4.tgz",
"integrity": "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g==",
"license": "MIT",
"dependencies": {
"html-to-text": "^9.0.5",
"prettier": "^3.5.3"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
"node_modules/@react-email/row": { "node_modules/@react-email/row": {
"version": "0.0.13", "version": "0.0.13",
"resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.13.tgz", "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.13.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@apollo/client": "^4.1.6", "@apollo/client": "^4.1.6",
"@react-email/components": "^1.0.10", "@react-email/components": "^1.0.10",
"@react-email/render": "^2.0.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.34.4", "framer-motion": "^12.34.4",
"graphql": "^16.13.1", "graphql": "^16.13.1",

View File

@@ -15,7 +15,10 @@ import {
CHECKOUT_BILLING_ADDRESS_UPDATE, CHECKOUT_BILLING_ADDRESS_UPDATE,
CHECKOUT_COMPLETE, CHECKOUT_COMPLETE,
CHECKOUT_EMAIL_UPDATE, CHECKOUT_EMAIL_UPDATE,
CHECKOUT_METADATA_UPDATE,
CHECKOUT_SHIPPING_METHOD_UPDATE,
} from "@/lib/saleor/mutations/Checkout"; } from "@/lib/saleor/mutations/Checkout";
import { GET_CHECKOUT_BY_ID } from "@/lib/saleor/queries/Checkout";
import type { Checkout } from "@/types/saleor"; import type { Checkout } from "@/types/saleor";
interface ShippingAddressUpdateResponse { interface ShippingAddressUpdateResponse {
@@ -46,6 +49,36 @@ interface EmailUpdateResponse {
}; };
} }
interface MetadataUpdateResponse {
updateMetadata?: {
item?: {
id: string;
metadata?: Array<{ key: string; value: string }>;
};
errors?: Array<{ message: string }>;
};
}
interface ShippingMethodUpdateResponse {
checkoutShippingMethodUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutQueryResponse {
checkout?: Checkout;
}
interface ShippingMethod {
id: string;
name: string;
price: {
amount: number;
currency: string;
};
}
interface AddressForm { interface AddressForm {
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -92,6 +125,10 @@ export default function CheckoutPage() {
email: "", email: "",
}); });
const [shippingMethods, setShippingMethods] = useState<ShippingMethod[]>([]);
const [selectedShippingMethod, setSelectedShippingMethod] = useState<string>("");
const [showShippingMethods, setShowShippingMethods] = useState(false);
const lines = getLines(); const lines = getLines();
const total = getTotal(); const total = getTotal();
@@ -101,6 +138,13 @@ export default function CheckoutPage() {
} }
}, [checkout, refreshCheckout]); }, [checkout, refreshCheckout]);
// Scroll to top when order is complete
useEffect(() => {
if (orderComplete) {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}, [orderComplete]);
const handleShippingChange = (field: keyof AddressForm, value: string) => { const handleShippingChange = (field: keyof AddressForm, value: string) => {
setShippingAddress((prev) => ({ ...prev, [field]: value })); setShippingAddress((prev) => ({ ...prev, [field]: value }));
if (sameAsShipping && field !== "email") { if (sameAsShipping && field !== "email") {
@@ -138,81 +182,169 @@ export default function CheckoutPage() {
setError(null); setError(null);
try { try {
const emailResult = await saleorClient.mutate<EmailUpdateResponse>({ // If we're showing shipping methods and one is selected, complete the order
mutation: CHECKOUT_EMAIL_UPDATE, if (showShippingMethods && selectedShippingMethod) {
variables: { console.log("Phase 2: Completing order with shipping method...");
checkoutId: checkout.id,
email: shippingAddress.email,
},
});
if (emailResult.data?.checkoutEmailUpdate?.errors && emailResult.data.checkoutEmailUpdate.errors.length > 0) { console.log("Step 1: Updating billing address...");
throw new Error(emailResult.data.checkoutEmailUpdate.errors[0].message); const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
} mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
variables: {
const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({ checkoutId: checkout.id,
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE, billingAddress: {
variables: { firstName: billingAddress.firstName,
checkoutId: checkout.id, lastName: billingAddress.lastName,
shippingAddress: { streetAddress1: billingAddress.streetAddress1,
firstName: shippingAddress.firstName, streetAddress2: billingAddress.streetAddress2,
lastName: shippingAddress.lastName, city: billingAddress.city,
streetAddress1: shippingAddress.streetAddress1, postalCode: billingAddress.postalCode,
streetAddress2: shippingAddress.streetAddress2, country: billingAddress.country,
city: shippingAddress.city, phone: billingAddress.phone,
postalCode: shippingAddress.postalCode, },
country: shippingAddress.country,
phone: shippingAddress.phone,
}, },
}, });
});
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) { if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message); throw new Error(`Billing address update failed: ${billingResult.data.checkoutBillingAddressUpdate.errors[0].message}`);
} }
console.log("Step 1: Billing address updated successfully");
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({ console.log("Step 2: Setting shipping method...");
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE, const shippingMethodResult = await saleorClient.mutate<ShippingMethodUpdateResponse>({
variables: { mutation: CHECKOUT_SHIPPING_METHOD_UPDATE,
checkoutId: checkout.id, variables: {
billingAddress: { checkoutId: checkout.id,
firstName: billingAddress.firstName, shippingMethodId: selectedShippingMethod,
lastName: billingAddress.lastName,
streetAddress1: billingAddress.streetAddress1,
streetAddress2: billingAddress.streetAddress2,
city: billingAddress.city,
postalCode: billingAddress.postalCode,
country: billingAddress.country,
phone: billingAddress.phone,
}, },
}, });
});
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) { if (shippingMethodResult.data?.checkoutShippingMethodUpdate?.errors && shippingMethodResult.data.checkoutShippingMethodUpdate.errors.length > 0) {
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message); throw new Error(`Shipping method update failed: ${shippingMethodResult.data.checkoutShippingMethodUpdate.errors[0].message}`);
} }
console.log("Step 2: Shipping method set successfully");
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({ console.log("Step 3: Saving phone number...");
mutation: CHECKOUT_COMPLETE, const metadataResult = await saleorClient.mutate<MetadataUpdateResponse>({
variables: { mutation: CHECKOUT_METADATA_UPDATE,
checkoutId: checkout.id, variables: {
}, checkoutId: checkout.id,
}); metadata: [
{ key: "phone", value: shippingAddress.phone },
{ key: "shippingPhone", value: shippingAddress.phone },
],
},
});
if (completeResult.data?.checkoutComplete?.errors && completeResult.data.checkoutComplete.errors.length > 0) { if (metadataResult.data?.updateMetadata?.errors && metadataResult.data.updateMetadata.errors.length > 0) {
throw new Error(completeResult.data.checkoutComplete.errors[0].message); console.warn("Failed to save phone metadata:", metadataResult.data.updateMetadata.errors);
} } else {
console.log("Step 3: Phone number saved successfully");
}
const order = completeResult.data?.checkoutComplete?.order; console.log("Step 4: Completing checkout...");
if (order) { const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
setOrderNumber(order.number); mutation: CHECKOUT_COMPLETE,
setOrderComplete(true); variables: {
checkoutId: checkout.id,
},
});
if (completeResult.data?.checkoutComplete?.errors && completeResult.data.checkoutComplete.errors.length > 0) {
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
}
const order = completeResult.data?.checkoutComplete?.order;
if (order) {
setOrderNumber(order.number);
setOrderComplete(true);
} else {
throw new Error(t("errorCreatingOrder"));
}
} else { } else {
throw new Error(t("errorCreatingOrder")); // Phase 1: Update email and address, then fetch shipping methods
console.log("Phase 1: Updating email and address...");
console.log("Step 1: Updating email...");
const emailResult = await saleorClient.mutate<EmailUpdateResponse>({
mutation: CHECKOUT_EMAIL_UPDATE,
variables: {
checkoutId: checkout.id,
email: shippingAddress.email,
},
});
if (emailResult.data?.checkoutEmailUpdate?.errors && emailResult.data.checkoutEmailUpdate.errors.length > 0) {
throw new Error(`Email update failed: ${emailResult.data.checkoutEmailUpdate.errors[0].message}`);
}
console.log("Step 1: Email updated successfully");
console.log("Step 2: Updating shipping address...");
console.log("Shipping address data:", {
firstName: shippingAddress.firstName,
lastName: shippingAddress.lastName,
streetAddress1: shippingAddress.streetAddress1,
city: shippingAddress.city,
postalCode: shippingAddress.postalCode,
country: shippingAddress.country,
phone: shippingAddress.phone,
});
const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
variables: {
checkoutId: checkout.id,
shippingAddress: {
firstName: shippingAddress.firstName,
lastName: shippingAddress.lastName,
streetAddress1: shippingAddress.streetAddress1,
streetAddress2: shippingAddress.streetAddress2,
city: shippingAddress.city,
postalCode: shippingAddress.postalCode,
country: shippingAddress.country,
phone: shippingAddress.phone,
},
},
});
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) {
throw new Error(`Shipping address update failed: ${shippingResult.data.checkoutShippingAddressUpdate.errors[0].message}`);
}
console.log("Step 2: Shipping address updated successfully");
// Query for checkout to get available shipping methods
console.log("Step 3: Fetching shipping methods...");
const checkoutQueryResult = await saleorClient.query<CheckoutQueryResponse>({
query: GET_CHECKOUT_BY_ID,
variables: {
id: checkout.id,
},
fetchPolicy: "network-only",
});
const availableMethods = checkoutQueryResult.data?.checkout?.shippingMethods || [];
console.log("Available shipping methods:", availableMethods);
if (availableMethods.length === 0) {
throw new Error(t("errorNoShippingMethods"));
}
setShippingMethods(availableMethods);
setShowShippingMethods(true);
// Don't complete yet - show shipping method selection
console.log("Phase 1 complete. Waiting for shipping method selection...");
} }
} catch (err: unknown) { } catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : null; console.error("Checkout error:", err);
setError(errorMessage || t("errorOccurred"));
if (err instanceof Error) {
if (err.name === "AbortError") {
setError("Request timed out. Please check your connection and try again.");
} else {
setError(err.message || t("errorOccurred"));
}
} else {
setError(t("errorOccurred"));
}
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -415,12 +547,49 @@ export default function CheckoutPage() {
</label> </label>
</div> </div>
{/* Shipping Method Selection */}
{showShippingMethods && shippingMethods.length > 0 && (
<div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">{t("shippingMethod")}</h2>
<div className="space-y-3">
{shippingMethods.map((method) => (
<label
key={method.id}
className={`flex items-center justify-between p-4 border rounded cursor-pointer transition-colors ${
selectedShippingMethod === method.id
? "border-foreground bg-background-ice"
: "border-border hover:border-foreground/50"
}`}
>
<div className="flex items-center gap-3">
<input
type="radio"
name="shippingMethod"
value={method.id}
checked={selectedShippingMethod === method.id}
onChange={(e) => setSelectedShippingMethod(e.target.value)}
className="w-4 h-4"
/>
<span className="font-medium">{method.name}</span>
</div>
<span className="text-foreground-muted">
{formatPrice(method.price.amount)}
</span>
</label>
))}
</div>
{!selectedShippingMethod && (
<p className="text-red-500 text-sm mt-2">{t("errorSelectShipping")}</p>
)}
</div>
)}
<button <button
type="submit" type="submit"
disabled={isLoading || lines.length === 0} disabled={isLoading || lines.length === 0 || (showShippingMethods && !selectedShippingMethod)}
className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50" className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
> >
{isLoading ? t("processing") : t("completeOrder", { total: formatPrice(total) })} {isLoading ? t("processing") : showShippingMethods ? t("completeOrder", { total: formatPrice(total) }) : t("continueToShipping")}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -7,6 +7,7 @@ import { OrderCancelled } from "@/emails/OrderCancelled";
import { OrderPaid } from "@/emails/OrderPaid"; import { OrderPaid } from "@/emails/OrderPaid";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
const DASHBOARD_URL = process.env.DASHBOARD_URL || "https://dashboard.manoonoils.com";
interface SaleorWebhookHeaders { interface SaleorWebhookHeaders {
"saleor-event": string; "saleor-event": string;
@@ -69,6 +70,7 @@ interface SaleorOrder {
} }
const SUPPORTED_EVENTS = [ const SUPPORTED_EVENTS = [
"ORDER_CREATED",
"ORDER_CONFIRMED", "ORDER_CONFIRMED",
"ORDER_FULLY_PAID", "ORDER_FULLY_PAID",
"ORDER_CANCELLED", "ORDER_CANCELLED",
@@ -141,6 +143,7 @@ async function handleOrderConfirmed(order: SaleorOrder) {
const customerName = getCustomerName(order); const customerName = getCustomerName(order);
const customerEmail = order.userEmail; const customerEmail = order.userEmail;
const phone = order.shippingAddress?.phone || order.billingAddress?.phone;
await sendEmailToCustomer({ await sendEmailToCustomer({
to: customerEmail, to: customerEmail,
@@ -168,7 +171,7 @@ async function handleOrderConfirmed(order: SaleorOrder) {
}); });
await sendEmailToAdmin({ await sendEmailToAdmin({
subject: `New Order #${order.number} - ${customerName}`, subject: `🎉 New Order #${order.number} - ${formatPrice(order.total.gross.amount, currency)}`,
react: OrderConfirmation({ react: OrderConfirmation({
language: "en", language: "en",
orderId: order.id, orderId: order.id,
@@ -178,7 +181,11 @@ async function handleOrderConfirmed(order: SaleorOrder) {
items: parseOrderItems(order.lines, currency), items: parseOrderItems(order.lines, currency),
total: formatPrice(order.total.gross.amount, currency), total: formatPrice(order.total.gross.amount, currency),
shippingAddress: formatAddress(order.shippingAddress), shippingAddress: formatAddress(order.shippingAddress),
billingAddress: formatAddress(order.billingAddress),
phone,
siteUrl: SITE_URL, siteUrl: SITE_URL,
dashboardUrl: DASHBOARD_URL,
isAdmin: true,
}), }),
eventType: "ORDER_CONFIRMED", eventType: "ORDER_CONFIRMED",
orderId: order.id, orderId: order.id,
@@ -360,6 +367,7 @@ async function handleSaleorWebhook(
} }
switch (event) { switch (event) {
case "ORDER_CREATED":
case "ORDER_CONFIRMED": case "ORDER_CONFIRMED":
await handleOrderConfirmed(order); await handleOrderConfirmed(order);
break; break;
@@ -379,6 +387,9 @@ async function handleSaleorWebhook(
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
console.log("=== WEBHOOK RECEIVED ===");
console.log("Timestamp:", new Date().toISOString());
const body = await request.json(); const body = await request.json();
const headers = request.headers; const headers = request.headers;
@@ -388,6 +399,14 @@ export async function POST(request: NextRequest) {
const apiUrl = headers.get("saleor-api-url"); const apiUrl = headers.get("saleor-api-url");
console.log(`Received webhook: ${event} from ${domain}`); console.log(`Received webhook: ${event} from ${domain}`);
console.log("Headers:", { event, domain, apiUrl, hasSignature: !!signature });
console.log("Payload keys:", Object.keys(body));
if (body.order) {
console.log("Order ID:", body.order.id);
console.log("Order number:", body.order.number);
console.log("User email:", body.order.userEmail);
}
if (!event) { if (!event) {
return NextResponse.json({ error: "Missing saleor-event header" }, { status: 400 }); return NextResponse.json({ error: "Missing saleor-event header" }, { status: 400 });

View File

@@ -49,7 +49,7 @@ export function BaseLayout({ children, previewText, language, siteUrl }: BaseLay
<Container style={styles.container}> <Container style={styles.container}>
<Section style={styles.logoSection}> <Section style={styles.logoSection}>
<Img <Img
src={`${siteUrl}/logo.png`} src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
width="150" width="150"
height="auto" height="auto"
alt="ManoonOils" alt="ManoonOils"

View File

@@ -17,7 +17,11 @@ interface OrderConfirmationProps {
items: OrderItem[]; items: OrderItem[];
total: string; total: string;
shippingAddress?: string; shippingAddress?: string;
billingAddress?: string;
phone?: string;
siteUrl: string; siteUrl: string;
dashboardUrl?: string;
isAdmin?: boolean;
} }
const translations: Record< const translations: Record<
@@ -34,6 +38,15 @@ const translations: Record<
shippingTo: string; shippingTo: string;
questions: string; questions: string;
thankYou: string; thankYou: string;
adminTitle: string;
adminPreview: string;
adminGreeting: string;
adminMessage: string;
customerLabel: string;
customerEmailLabel: string;
billingAddressLabel: string;
phoneLabel: string;
viewDashboard: string;
} }
> = { > = {
sr: { sr: {
@@ -48,6 +61,15 @@ const translations: Record<
shippingTo: "Adresa za dostavu", shippingTo: "Adresa za dostavu",
questions: "Imate pitanja? Pišite nam na support@manoonoils.com", questions: "Imate pitanja? Pišite nam na support@manoonoils.com",
thankYou: "Hvala Vam što kupujete kod nas!", thankYou: "Hvala Vam što kupujete kod nas!",
adminTitle: "Nova narudžbina!",
adminPreview: "Nova narudžbina je primljena",
adminGreeting: "Čestitamo na prodaji!",
adminMessage: "Nova narudžbina je upravo primljena. Detalji su ispod:",
customerLabel: "Kupac",
customerEmailLabel: "Email kupca",
billingAddressLabel: "Adresa za naplatu",
phoneLabel: "Telefon",
viewDashboard: "Pogledaj u Dashboardu",
}, },
en: { en: {
title: "Order Confirmation", title: "Order Confirmation",
@@ -62,6 +84,15 @@ const translations: Record<
shippingTo: "Shipping address", shippingTo: "Shipping address",
questions: "Questions? Email us at support@manoonoils.com", questions: "Questions? Email us at support@manoonoils.com",
thankYou: "Thank you for shopping with us!", thankYou: "Thank you for shopping with us!",
adminTitle: "New Order! 🎉",
adminPreview: "A new order has been received",
adminGreeting: "Congratulations on the sale!",
adminMessage: "A new order has just been placed. Details below:",
customerLabel: "Customer",
customerEmailLabel: "Customer Email",
billingAddressLabel: "Billing Address",
phoneLabel: "Phone",
viewDashboard: "View in Dashboard",
}, },
de: { de: {
title: "Bestellungsbestätigung", title: "Bestellungsbestätigung",
@@ -76,6 +107,15 @@ const translations: Record<
shippingTo: "Lieferadresse", shippingTo: "Lieferadresse",
questions: "Fragen? Schreiben Sie uns an support@manoonoils.com", questions: "Fragen? Schreiben Sie uns an support@manoonoils.com",
thankYou: "Vielen Dank für Ihren Einkauf!", thankYou: "Vielen Dank für Ihren Einkauf!",
adminTitle: "Neue Bestellung! 🎉",
adminPreview: "Eine neue Bestellung wurde erhalten",
adminGreeting: "Glückwunsch zum Verkauf!",
adminMessage: "Eine neue Bestellung wurde soeben aufgegeben. Details unten:",
customerLabel: "Kunde",
customerEmailLabel: "Kunden-E-Mail",
billingAddressLabel: "Rechnungsadresse",
phoneLabel: "Telefon",
viewDashboard: "Im Dashboard anzeigen",
}, },
fr: { fr: {
title: "Confirmation de commande", title: "Confirmation de commande",
@@ -90,6 +130,15 @@ const translations: Record<
shippingTo: "Adresse de livraison", shippingTo: "Adresse de livraison",
questions: "Questions? Écrivez-nous à support@manoonoils.com", questions: "Questions? Écrivez-nous à support@manoonoils.com",
thankYou: "Merci d'avoir Magasiné avec nous!", thankYou: "Merci d'avoir Magasiné avec nous!",
adminTitle: "Nouvelle commande! 🎉",
adminPreview: "Une nouvelle commande a été reçue",
adminGreeting: "Félicitations pour la vente!",
adminMessage: "Une nouvelle commande vient d'être passée. Détails ci-dessous:",
customerLabel: "Client",
customerEmailLabel: "Email du client",
billingAddressLabel: "Adresse de facturation",
phoneLabel: "Téléphone",
viewDashboard: "Voir dans le Dashboard",
}, },
}; };
@@ -102,10 +151,82 @@ export function OrderConfirmation({
items, items,
total, total,
shippingAddress, shippingAddress,
billingAddress,
phone,
siteUrl, siteUrl,
dashboardUrl,
isAdmin = false,
}: OrderConfirmationProps) { }: OrderConfirmationProps) {
const t = translations[language] || translations.en; const t = translations[language] || translations.en;
// For admin emails, always use English
const adminT = translations["en"];
if (isAdmin) {
return (
<BaseLayout previewText={adminT.adminPreview} language="en" siteUrl={siteUrl}>
<Text style={styles.title}>{adminT.adminTitle}</Text>
<Text style={styles.greeting}>{adminT.adminGreeting}</Text>
<Text style={styles.text}>{adminT.adminMessage}</Text>
<Section style={styles.orderInfo}>
<Text style={styles.orderNumber}>
<strong>{adminT.orderNumber}:</strong> {orderNumber}
</Text>
<Text style={styles.customerInfo}>
<strong>{adminT.customerLabel}:</strong> {customerName}
</Text>
<Text style={styles.customerInfo}>
<strong>{adminT.customerEmailLabel}:</strong> {customerEmail}
</Text>
{phone && (
<Text style={styles.customerInfo}>
<strong>{adminT.phoneLabel}:</strong> {phone}
</Text>
)}
</Section>
<Section style={styles.itemsSection}>
<Text style={styles.sectionTitle}>{adminT.items}</Text>
<Hr style={styles.hr} />
{items.map((item) => (
<Section key={item.id} style={styles.itemRow}>
<Text style={styles.itemName}>
{item.quantity}x {item.name}
</Text>
<Text style={styles.itemPrice}>{item.price}</Text>
</Section>
))}
<Hr style={styles.hr} />
<Section style={styles.totalRow}>
<Text style={styles.totalLabel}>{adminT.total}:</Text>
<Text style={styles.totalValue}>{total}</Text>
</Section>
</Section>
{shippingAddress && (
<Section style={styles.shippingSection}>
<Text style={styles.sectionTitle}>{adminT.shippingTo}</Text>
<Text style={styles.shippingAddress}>{shippingAddress}</Text>
</Section>
)}
{billingAddress && (
<Section style={styles.shippingSection}>
<Text style={styles.sectionTitle}>{adminT.billingAddressLabel}</Text>
<Text style={styles.shippingAddress}>{billingAddress}</Text>
</Section>
)}
<Section style={styles.buttonSection}>
<Button href={`${dashboardUrl}/orders/${orderId}`} style={styles.button}>
{adminT.viewDashboard}
</Button>
</Section>
</BaseLayout>
);
}
return ( return (
<BaseLayout previewText={t.preview} language={language} siteUrl={siteUrl}> <BaseLayout previewText={t.preview} language={language} siteUrl={siteUrl}>
<Text style={styles.title}>{t.title}</Text> <Text style={styles.title}>{t.title}</Text>
@@ -187,7 +308,12 @@ const styles = {
orderNumber: { orderNumber: {
fontSize: "14px", fontSize: "14px",
color: "#333333", color: "#333333",
margin: "0", margin: "0 0 8px 0",
},
customerInfo: {
fontSize: "14px",
color: "#333333",
margin: "0 0 4px 0",
}, },
itemsSection: { itemsSection: {
marginBottom: "20px", marginBottom: "20px",

View File

@@ -345,6 +345,7 @@
"emailRequired": "Erforderlich für Bestellbestätigung", "emailRequired": "Erforderlich für Bestellbestätigung",
"phoneRequired": "Erforderlich für Lieferkoordination", "phoneRequired": "Erforderlich für Lieferkoordination",
"shippingAddress": "Lieferadresse", "shippingAddress": "Lieferadresse",
"shippingMethod": "Versandart",
"country": "Land", "country": "Land",
"firstName": "Vorname", "firstName": "Vorname",
"lastName": "Nachname", "lastName": "Nachname",

View File

@@ -391,6 +391,7 @@
"emailRequired": "Required for order confirmation", "emailRequired": "Required for order confirmation",
"phoneRequired": "Required for delivery coordination", "phoneRequired": "Required for delivery coordination",
"shippingAddress": "Shipping Address", "shippingAddress": "Shipping Address",
"shippingMethod": "Shipping Method",
"country": "Country", "country": "Country",
"firstName": "First Name", "firstName": "First Name",
"lastName": "Last Name", "lastName": "Last Name",
@@ -417,8 +418,11 @@
"errorNoCheckout": "No active checkout. Please try again.", "errorNoCheckout": "No active checkout. Please try again.",
"errorEmailRequired": "Please enter a valid email address.", "errorEmailRequired": "Please enter a valid email address.",
"errorFieldsRequired": "Please fill in all required fields.", "errorFieldsRequired": "Please fill in all required fields.",
"errorNoShippingMethods": "No shipping methods available for this address. Please check your address or contact support.",
"errorSelectShipping": "Please select a shipping method.",
"errorOccurred": "An error occurred during checkout.", "errorOccurred": "An error occurred during checkout.",
"errorCreatingOrder": "Failed to create order.", "errorCreatingOrder": "Failed to create order.",
"continueToShipping": "Continue to Shipping",
"orderConfirmed": "Order Confirmed!", "orderConfirmed": "Order Confirmed!",
"thankYou": "Thank you for your purchase.", "thankYou": "Thank you for your purchase.",
"orderNumber": "Order Number", "orderNumber": "Order Number",

View File

@@ -345,6 +345,7 @@
"emailRequired": "Requis pour la confirmation de commande", "emailRequired": "Requis pour la confirmation de commande",
"phoneRequired": "Requis pour la coordination de livraison", "phoneRequired": "Requis pour la coordination de livraison",
"shippingAddress": "Adresse de Livraison", "shippingAddress": "Adresse de Livraison",
"shippingMethod": "Méthode de livraison",
"country": "Pays", "country": "Pays",
"firstName": "Prénom", "firstName": "Prénom",
"lastName": "Nom", "lastName": "Nom",

View File

@@ -391,6 +391,7 @@
"emailRequired": "Potrebno za potvrdu narudžbine", "emailRequired": "Potrebno za potvrdu narudžbine",
"phoneRequired": "Potrebno za koordinaciju dostave", "phoneRequired": "Potrebno za koordinaciju dostave",
"shippingAddress": "Adresa za dostavu", "shippingAddress": "Adresa za dostavu",
"shippingMethod": "Način dostave",
"country": "Država", "country": "Država",
"firstName": "Ime", "firstName": "Ime",
"lastName": "Prezime", "lastName": "Prezime",

View File

@@ -1,4 +1,5 @@
import { Resend } from "resend"; import { Resend } from "resend";
import { render } from "@react-email/render";
let resendClient: Resend | null = null; let resendClient: Resend | null = null;
@@ -30,17 +31,22 @@ export async function sendEmail({
idempotencyKey?: string; idempotencyKey?: string;
}) { }) {
const resend = getResendClient(); const resend = getResendClient();
const { data, error } = await resend.emails.send(
{ // Render React component to HTML
from: "ManoonOils <support@manoonoils.com>", const html = await render(react, {
to: Array.isArray(to) ? to : [to], pretty: true,
subject, });
react,
text, const { data, error } = await resend.emails.send({
tags, from: "ManoonOils <support@mail.manoonoils.com>",
...(idempotencyKey && { idempotencyKey }), replyTo: "support@manoonoils.com",
} to: Array.isArray(to) ? to : [to],
); subject,
html,
text,
tags,
...(idempotencyKey && { idempotencyKey }),
});
if (error) { if (error) {
console.error("Failed to send email:", error); console.error("Failed to send email:", error);

View File

@@ -152,3 +152,24 @@ export const CHECKOUT_EMAIL_UPDATE = gql`
} }
${CHECKOUT_FRAGMENT} ${CHECKOUT_FRAGMENT}
`; `;
export const CHECKOUT_METADATA_UPDATE = gql`
mutation CheckoutMetadataUpdate($checkoutId: ID!, $metadata: [MetadataInput!]!) {
updateMetadata(id: $checkoutId, input: $metadata) {
item {
... on Checkout {
id
metadata {
key
value
}
}
}
errors {
field
message
code
}
}
}
`;