"use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; import { useTranslations, useLocale } from "next-intl"; import Header from "@/components/layout/Header"; import Footer from "@/components/layout/Footer"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { formatPrice } from "@/lib/saleor"; import { saleorClient } from "@/lib/saleor/client"; import { useAnalytics } from "@/lib/analytics"; import { CHECKOUT_SHIPPING_ADDRESS_UPDATE, } from "@/lib/saleor/mutations/Checkout"; import { PaymentSection } from "./components/PaymentSection"; import { DEFAULT_PAYMENT_METHOD } from "@/lib/config/paymentMethods"; import { GET_CHECKOUT_BY_ID } from "@/lib/saleor/queries/Checkout"; import type { Checkout } from "@/types/saleor"; import { createCheckoutService, type Address } from "@/lib/services/checkoutService"; import { useShippingMethodSelector } from "@/lib/hooks/useShippingMethodSelector"; interface ShippingAddressUpdateResponse { checkoutShippingAddressUpdate?: { 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; streetAddress1: string; streetAddress2: string; city: string; postalCode: string; country: string; phone: string; email: string; } export default function CheckoutPage() { const t = useTranslations("Checkout"); const locale = useLocale(); const router = useRouter(); const { checkout, refreshCheckout, clearCheckout, getLines, getTotal } = useSaleorCheckoutStore(); const { trackCheckoutStarted, trackCheckoutStep, trackOrderCompleted, identifyUser } = useAnalytics(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [orderComplete, setOrderComplete] = useState(false); const [orderNumber, setOrderNumber] = useState(null); const [sameAsShipping, setSameAsShipping] = useState(true); const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(DEFAULT_PAYMENT_METHOD); const [shippingAddress, setShippingAddress] = useState({ firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", postalCode: "", country: "RS", phone: "", email: "", }); const [billingAddress, setBillingAddress] = useState({ firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", postalCode: "", country: "RS", phone: "", email: "", }); const [shippingMethods, setShippingMethods] = useState([]); const [selectedShippingMethod, setSelectedShippingMethod] = useState(""); const [isLoadingShipping, setIsLoadingShipping] = useState(false); // Hook to manage shipping method selection (both manual and auto) const { selectShippingMethodWithApi } = useShippingMethodSelector({ checkoutId: checkout?.id ?? null, onSelect: setSelectedShippingMethod, onRefresh: refreshCheckout, }); const lines = getLines(); // Use checkout.totalPrice directly for reactive updates when shipping method changes const total = checkout?.totalPrice?.gross?.amount || getTotal(); // Debounced shipping method fetching useEffect(() => { if (!checkout) return; // Check if address is complete enough to fetch shipping methods const isAddressComplete = shippingAddress.firstName && shippingAddress.lastName && shippingAddress.streetAddress1 && shippingAddress.city && shippingAddress.postalCode && shippingAddress.country; if (!isAddressComplete) { setShippingMethods([]); return; } const timer = setTimeout(async () => { setIsLoadingShipping(true); try { console.log("Fetching shipping methods..."); // First update the shipping address 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, }, }, }); // Then query for 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); setShippingMethods(availableMethods); // Auto-select first method if none selected if (availableMethods.length > 0 && !selectedShippingMethod) { const firstMethodId = availableMethods[0].id; // Use the hook to both update UI and call API await selectShippingMethodWithApi(firstMethodId); } } catch (err) { console.error("Error fetching shipping methods:", err); } finally { setIsLoadingShipping(false); } }, 500); // 500ms debounce return () => clearTimeout(timer); }, [checkout, shippingAddress]); useEffect(() => { if (!checkout) { refreshCheckout(); } }, [checkout, refreshCheckout]); // Track checkout started when page loads useEffect(() => { if (checkout) { const lines = getLines(); const total = getTotal(); trackCheckoutStarted({ total, currency: "RSD", item_count: lines.reduce((sum, line) => sum + line.quantity, 0), items: lines.map(line => ({ id: line.variant.id, name: line.variant.product.name, quantity: line.quantity, price: line.variant.pricing?.price?.gross?.amount || 0, currency: line.variant.pricing?.price?.gross?.currency || "RSD", })), }); } }, [checkout]); // 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") { setBillingAddress((prev) => ({ ...prev, [field]: value })); } }; const handleBillingChange = (field: keyof AddressForm, value: string) => { setBillingAddress((prev) => ({ ...prev, [field]: value })); }; const handleEmailChange = (value: string) => { setShippingAddress((prev) => ({ ...prev, email: value })); }; const handleShippingMethodSelect = async (methodId: string) => { await selectShippingMethodWithApi(methodId); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!checkout) { setError(t("errorNoCheckout")); return; } // Validate all required fields if (!shippingAddress.email || !shippingAddress.email.includes("@")) { setError(t("errorEmailRequired")); return; } if (!shippingAddress.phone || shippingAddress.phone.length < 8) { setError(t("errorPhoneRequired")); return; } if (!shippingAddress.firstName || !shippingAddress.lastName || !shippingAddress.streetAddress1 || !shippingAddress.city || !shippingAddress.postalCode) { setError(t("errorFieldsRequired")); return; } if (!selectedShippingMethod) { setError(t("errorSelectShipping")); return; } if (!selectedPaymentMethod) { setError(t("errorSelectPayment")); return; } setIsLoading(true); setError(null); try { console.log("Completing order via CheckoutService..."); // Create checkout service instance const checkoutService = createCheckoutService(checkout.id); // Transform form data to service types const serviceShippingAddress: Address = { firstName: shippingAddress.firstName, lastName: shippingAddress.lastName, streetAddress1: shippingAddress.streetAddress1, streetAddress2: shippingAddress.streetAddress2, city: shippingAddress.city, postalCode: shippingAddress.postalCode, country: shippingAddress.country, phone: shippingAddress.phone, }; const serviceBillingAddress: Address = { firstName: billingAddress.firstName, lastName: billingAddress.lastName, streetAddress1: billingAddress.streetAddress1, streetAddress2: billingAddress.streetAddress2, city: billingAddress.city, postalCode: billingAddress.postalCode, country: billingAddress.country, phone: billingAddress.phone, }; // Execute checkout pipeline const result = await checkoutService.execute({ email: shippingAddress.email, shippingAddress: serviceShippingAddress, billingAddress: serviceBillingAddress, shippingMethodId: selectedShippingMethod, languageCode: locale.toUpperCase(), metadata: { phone: shippingAddress.phone, shippingPhone: shippingAddress.phone, userLanguage: locale, userLocale: locale, }, }); if (!result.success || !result.order) { // Handle specific error types if (result.error === "CHECKOUT_EXPIRED") { console.error("Checkout not found, clearing cart..."); localStorage.removeItem('cart'); localStorage.removeItem('checkoutId'); window.location.href = `/${locale}/products`; return; } throw new Error(result.error || t("errorCreatingOrder")); } // Success! setOrderNumber(result.order.number); setOrderComplete(true); // Track order completion BEFORE clearing checkout const lines = getLines(); const total = getTotal(); console.log("[Checkout] Order total before tracking:", total, "RSD"); trackOrderCompleted({ order_id: checkout.id, order_number: result.order.number, total, currency: "RSD", item_count: lines.reduce((sum, line) => sum + line.quantity, 0), shipping_cost: shippingMethods.find(m => m.id === selectedShippingMethod)?.price.amount, customer_email: shippingAddress.email, }); // Clear the checkout/cart from the store clearCheckout(); // Identify the user identifyUser({ profileId: shippingAddress.email, email: shippingAddress.email, firstName: shippingAddress.firstName, lastName: shippingAddress.lastName, }); console.log("Order completed successfully:", result.order.number); } catch (err: unknown) { 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); } }; if (orderComplete) { return ( <>

{t("orderConfirmed")}

{t("thankYou")}

{orderNumber && (

{t("orderNumber")}

#{orderNumber}

)}

{t("confirmationEmail")}

{t("continueShoppingBtn")}
); } return ( <>

{t("checkout")}

{error && (
{error}
)}

{t("contactInfo")}

handleEmailChange(e.target.value)} className="w-full border border-border px-4 py-2 rounded" placeholder="email@example.com" />

{t("emailRequired")}

handleShippingChange("phone", e.target.value)} className="w-full border border-border px-4 py-2 rounded" placeholder="+381..." />

{t("phoneRequired")}

{t("shippingAddress")}

handleShippingChange("firstName", e.target.value)} className="w-full border border-border px-4 py-2 rounded" />
handleShippingChange("lastName", e.target.value)} className="w-full border border-border px-4 py-2 rounded" />
handleShippingChange("streetAddress1", e.target.value)} className="w-full border border-border px-4 py-2 rounded" />
handleShippingChange("streetAddress2", e.target.value)} placeholder={t("streetAddressOptional")} className="w-full border border-border px-4 py-2 rounded" />
handleShippingChange("city", e.target.value)} className="w-full border border-border px-4 py-2 rounded" />
handleShippingChange("postalCode", e.target.value)} className="w-full border border-border px-4 py-2 rounded" />
{/* Shipping Method Selection */}

{t("shippingMethod")}

{isLoadingShipping ? (
{t("loadingShippingMethods")}
) : shippingMethods.length > 0 ? (
{shippingMethods.map((method) => ( ))}
) : (

{t("enterAddressForShipping")}

)}
{/* Payment Method Section */} {/* Money Back Guarantee Trust Badge */}
{t("moneyBackGuarantee")}

{t("orderSummary")}

{lines.length === 0 ? (

{t("yourCartEmpty")}

) : ( <>
{lines.map((line) => (
{line.variant.product.media[0]?.url && ( {line.variant.product.name} )}

{line.variant.product.name}

{t("qty")}: {line.quantity}

{formatPrice(line.totalPrice.gross.amount)}

))}
{t("subtotal")} {formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}
{selectedShippingMethod && (
{t("shipping")} {formatPrice(shippingMethods.find(m => m.id === selectedShippingMethod)?.price.amount || 0)}
)}
{t("total")} {formatPrice(total)}
)}
); }