From 5576946829d5b027049898bb42410fda2f3809f3 Mon Sep 17 00:00:00 2001 From: Unchained Date: Wed, 25 Mar 2026 14:13:34 +0200 Subject: [PATCH] feat(emails): implement transactional email system with Resend - 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 --- package-lock.json | 35 ++-- package.json | 1 + src/app/[locale]/checkout/page.tsx | 299 +++++++++++++++++++++------ src/app/api/webhooks/saleor/route.ts | 21 +- src/emails/BaseLayout.tsx | 2 +- src/emails/OrderConfirmation.tsx | 128 +++++++++++- src/i18n/messages/de.json | 1 + src/i18n/messages/en.json | 4 + src/i18n/messages/fr.json | 1 + src/i18n/messages/sr.json | 1 + src/lib/resend.ts | 28 ++- src/lib/saleor/mutations/Checkout.ts | 21 ++ 12 files changed, 446 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index b959dc9..3789226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2bb0bc8..118a408 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/[locale]/checkout/page.tsx b/src/app/[locale]/checkout/page.tsx index 7b34a26..19c8b6a 100644 --- a/src/app/[locale]/checkout/page.tsx +++ b/src/app/[locale]/checkout/page.tsx @@ -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([]); + const [selectedShippingMethod, setSelectedShippingMethod] = useState(""); + 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({ - 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({ - 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({ + 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({ - 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({ + 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({ - mutation: CHECKOUT_COMPLETE, - variables: { - checkoutId: checkout.id, - }, - }); + console.log("Step 3: Saving phone number..."); + const metadataResult = await saleorClient.mutate({ + 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({ + 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({ + 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({ + 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({ + 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() { + {/* Shipping Method Selection */} + {showShippingMethods && shippingMethods.length > 0 && ( +
+

{t("shippingMethod")}

+
+ {shippingMethods.map((method) => ( + + ))} +
+ {!selectedShippingMethod && ( +

{t("errorSelectShipping")}

+ )} +
+ )} + diff --git a/src/app/api/webhooks/saleor/route.ts b/src/app/api/webhooks/saleor/route.ts index 52ccb4b..ff13fec 100644 --- a/src/app/api/webhooks/saleor/route.ts +++ b/src/app/api/webhooks/saleor/route.ts @@ -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 }); diff --git a/src/emails/BaseLayout.tsx b/src/emails/BaseLayout.tsx index e878b79..3bc4a78 100644 --- a/src/emails/BaseLayout.tsx +++ b/src/emails/BaseLayout.tsx @@ -49,7 +49,7 @@ export function BaseLayout({ children, previewText, language, siteUrl }: BaseLay
ManoonOils = { 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 ( + + {adminT.adminTitle} + {adminT.adminGreeting} + {adminT.adminMessage} + +
+ + {adminT.orderNumber}: {orderNumber} + + + {adminT.customerLabel}: {customerName} + + + {adminT.customerEmailLabel}: {customerEmail} + + {phone && ( + + {adminT.phoneLabel}: {phone} + + )} +
+ +
+ {adminT.items} +
+ {items.map((item) => ( +
+ + {item.quantity}x {item.name} + + {item.price} +
+ ))} +
+
+ {adminT.total}: + {total} +
+
+ + {shippingAddress && ( +
+ {adminT.shippingTo} + {shippingAddress} +
+ )} + + {billingAddress && ( +
+ {adminT.billingAddressLabel} + {billingAddress} +
+ )} + +
+ +
+
+ ); + } + return ( {t.title} @@ -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", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index ec3e40d..cdc1791 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -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", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index d276188..1237f89 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -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", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 2960399..a03ea26 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -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", diff --git a/src/i18n/messages/sr.json b/src/i18n/messages/sr.json index b159123..a84fb1f 100644 --- a/src/i18n/messages/sr.json +++ b/src/i18n/messages/sr.json @@ -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", diff --git a/src/lib/resend.ts b/src/lib/resend.ts index a61c130..45d95e3 100644 --- a/src/lib/resend.ts +++ b/src/lib/resend.ts @@ -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 ", - 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 ", + 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); diff --git a/src/lib/saleor/mutations/Checkout.ts b/src/lib/saleor/mutations/Checkout.ts index 4095298..ed8d2af 100644 --- a/src/lib/saleor/mutations/Checkout.ts +++ b/src/lib/saleor/mutations/Checkout.ts @@ -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 + } + } + } +`;