feat(emails): implement transactional email system with Resend
Some checks failed
Build and Deploy / build (push) Has been cancelled
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:
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user