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": {
"@apollo/client": "^4.1.6",
"@react-email/components": "^1.0.10",
"@react-email/render": "^2.0.4",
"clsx": "^2.1.1",
"framer-motion": "^12.34.4",
"graphql": "^16.13.1",
@@ -1743,23 +1744,6 @@
"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": {
"version": "0.0.16",
"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"
}
},
"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": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.13.tgz",

View File

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

View File

@@ -15,7 +15,10 @@ import {
CHECKOUT_BILLING_ADDRESS_UPDATE,
CHECKOUT_COMPLETE,
CHECKOUT_EMAIL_UPDATE,
CHECKOUT_METADATA_UPDATE,
CHECKOUT_SHIPPING_METHOD_UPDATE,
} from "@/lib/saleor/mutations/Checkout";
import { GET_CHECKOUT_BY_ID } from "@/lib/saleor/queries/Checkout";
import type { Checkout } from "@/types/saleor";
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 {
firstName: string;
lastName: string;
@@ -92,6 +125,10 @@ export default function CheckoutPage() {
email: "",
});
const [shippingMethods, setShippingMethods] = useState<ShippingMethod[]>([]);
const [selectedShippingMethod, setSelectedShippingMethod] = useState<string>("");
const [showShippingMethods, setShowShippingMethods] = useState(false);
const lines = getLines();
const total = getTotal();
@@ -101,6 +138,13 @@ export default function CheckoutPage() {
}
}, [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) => {
setShippingAddress((prev) => ({ ...prev, [field]: value }));
if (sameAsShipping && field !== "email") {
@@ -138,81 +182,169 @@ export default function CheckoutPage() {
setError(null);
try {
const emailResult = await saleorClient.mutate<EmailUpdateResponse>({
mutation: CHECKOUT_EMAIL_UPDATE,
variables: {
checkoutId: checkout.id,
email: shippingAddress.email,
},
});
// If we're showing shipping methods and one is selected, complete the order
if (showShippingMethods && selectedShippingMethod) {
console.log("Phase 2: Completing order with shipping method...");
if (emailResult.data?.checkoutEmailUpdate?.errors && emailResult.data.checkoutEmailUpdate.errors.length > 0) {
throw new Error(emailResult.data.checkoutEmailUpdate.errors[0].message);
}
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,
console.log("Step 1: Updating billing address...");
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
variables: {
checkoutId: checkout.id,
billingAddress: {
firstName: billingAddress.firstName,
lastName: billingAddress.lastName,
streetAddress1: billingAddress.streetAddress1,
streetAddress2: billingAddress.streetAddress2,
city: billingAddress.city,
postalCode: billingAddress.postalCode,
country: billingAddress.country,
phone: billingAddress.phone,
},
},
},
});
});
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) {
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
}
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
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>({
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
variables: {
checkoutId: checkout.id,
billingAddress: {
firstName: billingAddress.firstName,
lastName: billingAddress.lastName,
streetAddress1: billingAddress.streetAddress1,
streetAddress2: billingAddress.streetAddress2,
city: billingAddress.city,
postalCode: billingAddress.postalCode,
country: billingAddress.country,
phone: billingAddress.phone,
console.log("Step 2: Setting shipping method...");
const shippingMethodResult = await saleorClient.mutate<ShippingMethodUpdateResponse>({
mutation: CHECKOUT_SHIPPING_METHOD_UPDATE,
variables: {
checkoutId: checkout.id,
shippingMethodId: selectedShippingMethod,
},
},
});
});
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
}
if (shippingMethodResult.data?.checkoutShippingMethodUpdate?.errors && shippingMethodResult.data.checkoutShippingMethodUpdate.errors.length > 0) {
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>({
mutation: CHECKOUT_COMPLETE,
variables: {
checkoutId: checkout.id,
},
});
console.log("Step 3: Saving phone number...");
const metadataResult = await saleorClient.mutate<MetadataUpdateResponse>({
mutation: CHECKOUT_METADATA_UPDATE,
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) {
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
}
if (metadataResult.data?.updateMetadata?.errors && metadataResult.data.updateMetadata.errors.length > 0) {
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;
if (order) {
setOrderNumber(order.number);
setOrderComplete(true);
console.log("Step 4: Completing checkout...");
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
mutation: CHECKOUT_COMPLETE,
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 {
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) {
const errorMessage = err instanceof Error ? err.message : null;
setError(errorMessage || t("errorOccurred"));
console.error("Checkout error:", err);
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 {
setIsLoading(false);
}
@@ -415,12 +547,49 @@ export default function CheckoutPage() {
</label>
</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
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"
>
{isLoading ? t("processing") : t("completeOrder", { total: formatPrice(total) })}
{isLoading ? t("processing") : showShippingMethods ? t("completeOrder", { total: formatPrice(total) }) : t("continueToShipping")}
</button>
</form>
</div>

View File

@@ -7,6 +7,7 @@ import { OrderCancelled } from "@/emails/OrderCancelled";
import { OrderPaid } from "@/emails/OrderPaid";
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 {
"saleor-event": string;
@@ -69,6 +70,7 @@ interface SaleorOrder {
}
const SUPPORTED_EVENTS = [
"ORDER_CREATED",
"ORDER_CONFIRMED",
"ORDER_FULLY_PAID",
"ORDER_CANCELLED",
@@ -141,6 +143,7 @@ async function handleOrderConfirmed(order: SaleorOrder) {
const customerName = getCustomerName(order);
const customerEmail = order.userEmail;
const phone = order.shippingAddress?.phone || order.billingAddress?.phone;
await sendEmailToCustomer({
to: customerEmail,
@@ -168,7 +171,7 @@ async function handleOrderConfirmed(order: SaleorOrder) {
});
await sendEmailToAdmin({
subject: `New Order #${order.number} - ${customerName}`,
subject: `🎉 New Order #${order.number} - ${formatPrice(order.total.gross.amount, currency)}`,
react: OrderConfirmation({
language: "en",
orderId: order.id,
@@ -178,7 +181,11 @@ async function handleOrderConfirmed(order: SaleorOrder) {
items: parseOrderItems(order.lines, currency),
total: formatPrice(order.total.gross.amount, currency),
shippingAddress: formatAddress(order.shippingAddress),
billingAddress: formatAddress(order.billingAddress),
phone,
siteUrl: SITE_URL,
dashboardUrl: DASHBOARD_URL,
isAdmin: true,
}),
eventType: "ORDER_CONFIRMED",
orderId: order.id,
@@ -360,6 +367,7 @@ async function handleSaleorWebhook(
}
switch (event) {
case "ORDER_CREATED":
case "ORDER_CONFIRMED":
await handleOrderConfirmed(order);
break;
@@ -379,6 +387,9 @@ async function handleSaleorWebhook(
export async function POST(request: NextRequest) {
try {
console.log("=== WEBHOOK RECEIVED ===");
console.log("Timestamp:", new Date().toISOString());
const body = await request.json();
const headers = request.headers;
@@ -388,6 +399,14 @@ export async function POST(request: NextRequest) {
const apiUrl = headers.get("saleor-api-url");
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) {
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}>
<Section style={styles.logoSection}>
<Img
src={`${siteUrl}/logo.png`}
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
width="150"
height="auto"
alt="ManoonOils"

View File

@@ -17,7 +17,11 @@ interface OrderConfirmationProps {
items: OrderItem[];
total: string;
shippingAddress?: string;
billingAddress?: string;
phone?: string;
siteUrl: string;
dashboardUrl?: string;
isAdmin?: boolean;
}
const translations: Record<
@@ -34,6 +38,15 @@ const translations: Record<
shippingTo: string;
questions: string;
thankYou: string;
adminTitle: string;
adminPreview: string;
adminGreeting: string;
adminMessage: string;
customerLabel: string;
customerEmailLabel: string;
billingAddressLabel: string;
phoneLabel: string;
viewDashboard: string;
}
> = {
sr: {
@@ -48,6 +61,15 @@ const translations: Record<
shippingTo: "Adresa za dostavu",
questions: "Imate pitanja? Pišite nam na support@manoonoils.com",
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: {
title: "Order Confirmation",
@@ -62,6 +84,15 @@ const translations: Record<
shippingTo: "Shipping address",
questions: "Questions? Email us at support@manoonoils.com",
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: {
title: "Bestellungsbestätigung",
@@ -76,6 +107,15 @@ const translations: Record<
shippingTo: "Lieferadresse",
questions: "Fragen? Schreiben Sie uns an support@manoonoils.com",
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: {
title: "Confirmation de commande",
@@ -90,6 +130,15 @@ const translations: Record<
shippingTo: "Adresse de livraison",
questions: "Questions? Écrivez-nous à support@manoonoils.com",
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,
total,
shippingAddress,
billingAddress,
phone,
siteUrl,
dashboardUrl,
isAdmin = false,
}: OrderConfirmationProps) {
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 (
<BaseLayout previewText={t.preview} language={language} siteUrl={siteUrl}>
<Text style={styles.title}>{t.title}</Text>
@@ -187,7 +308,12 @@ const styles = {
orderNumber: {
fontSize: "14px",
color: "#333333",
margin: "0",
margin: "0 0 8px 0",
},
customerInfo: {
fontSize: "14px",
color: "#333333",
margin: "0 0 4px 0",
},
itemsSection: {
marginBottom: "20px",

View File

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

View File

@@ -391,6 +391,7 @@
"emailRequired": "Required for order confirmation",
"phoneRequired": "Required for delivery coordination",
"shippingAddress": "Shipping Address",
"shippingMethod": "Shipping Method",
"country": "Country",
"firstName": "First Name",
"lastName": "Last Name",
@@ -417,8 +418,11 @@
"errorNoCheckout": "No active checkout. Please try again.",
"errorEmailRequired": "Please enter a valid email address.",
"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.",
"errorCreatingOrder": "Failed to create order.",
"continueToShipping": "Continue to Shipping",
"orderConfirmed": "Order Confirmed!",
"thankYou": "Thank you for your purchase.",
"orderNumber": "Order Number",

View File

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

View File

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

View File

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

View File

@@ -152,3 +152,24 @@ export const CHECKOUT_EMAIL_UPDATE = gql`
}
${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
}
}
}
`;