From d43481716de976f454e8eaacaaa65f3473e3c158 Mon Sep 17 00:00:00 2001 From: Unchained Date: Sat, 21 Mar 2026 12:45:09 +0200 Subject: [PATCH] feat(saleor): Phase 4 - Checkout Flow - Create checkout page with form validation - Implement shipping/billing address forms - Add Cash on Delivery (COD) payment method - Integrate Saleor checkout completion mutation - Add order success page with confirmation - Handle checkout errors gracefully - Display order summary with line items --- src/app/checkout/page.tsx | 464 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 src/app/checkout/page.tsx diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx new file mode 100644 index 0000000..5fa8aad --- /dev/null +++ b/src/app/checkout/page.tsx @@ -0,0 +1,464 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; +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 { + CHECKOUT_SHIPPING_ADDRESS_UPDATE, + CHECKOUT_BILLING_ADDRESS_UPDATE, + CHECKOUT_COMPLETE, +} from "@/lib/saleor/mutations/Checkout"; + +interface AddressForm { + firstName: string; + lastName: string; + streetAddress1: string; + streetAddress2: string; + city: string; + postalCode: string; + phone: string; +} + +export default function CheckoutPage() { + const router = useRouter(); + const { checkout, refreshCheckout, getLines, getTotal } = useSaleorCheckoutStore(); + 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 [shippingAddress, setShippingAddress] = useState({ + firstName: "", + lastName: "", + streetAddress1: "", + streetAddress2: "", + city: "", + postalCode: "", + phone: "", + }); + const [billingAddress, setBillingAddress] = useState({ + firstName: "", + lastName: "", + streetAddress1: "", + streetAddress2: "", + city: "", + postalCode: "", + phone: "", + }); + + const lines = getLines(); + const total = getTotal(); + + useEffect(() => { + if (!checkout) { + refreshCheckout(); + } + }, [checkout, refreshCheckout]); + + // Redirect if cart is empty + useEffect(() => { + if (lines.length === 0 && !orderComplete) { + // Optionally redirect to cart or products + // router.push("/products"); + } + }, [lines, orderComplete, router]); + + const handleShippingChange = (field: keyof AddressForm, value: string) => { + setShippingAddress((prev) => ({ ...prev, [field]: value })); + if (sameAsShipping) { + setBillingAddress((prev) => ({ ...prev, [field]: value })); + } + }; + + const handleBillingChange = (field: keyof AddressForm, value: string) => { + setBillingAddress((prev) => ({ ...prev, [field]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!checkout) { + setError("No active checkout. Please try again."); + return; + } + + setIsLoading(true); + setError(null); + + try { + // Update shipping address + const shippingResult = await saleorClient.mutate({ + mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE, + variables: { + checkoutId: checkout.id, + shippingAddress: { + ...shippingAddress, + country: "RS", // Serbia + }, + }, + }); + + if (shippingResult.data?.checkoutShippingAddressUpdate?.errors?.length > 0) { + throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message); + } + + // Update billing address + const billingResult = await saleorClient.mutate({ + mutation: CHECKOUT_BILLING_ADDRESS_UPDATE, + variables: { + checkoutId: checkout.id, + billingAddress: { + ...billingAddress, + country: "RS", + }, + }, + }); + + if (billingResult.data?.checkoutBillingAddressUpdate?.errors?.length > 0) { + throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message); + } + + // Complete checkout (creates order) + const completeResult = await saleorClient.mutate({ + mutation: CHECKOUT_COMPLETE, + variables: { + checkoutId: checkout.id, + }, + }); + + if (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("Failed to create order"); + } + } catch (err: any) { + setError(err.message || "An error occurred during checkout"); + } finally { + setIsLoading(false); + } + }; + + // Order Success Page + if (orderComplete) { + return ( +
+
+
+
+
+
+ + + +
+

Order Confirmed!

+

Thank you for your purchase.

+
+ + {orderNumber && ( +
+

Order Number

+

#{orderNumber}

+
+ )} + +

+ You will receive a confirmation email shortly. We will contact you to arrange Cash on Delivery. +

+ + + Continue Shopping + +
+
+
+
+ ); + } + + return ( +
+
+ +
+
+

Checkout

+ + {error && ( +
+ {error} +
+ )} + +
+ {/* Checkout Form */} +
+
+ {/* Shipping Address */} +
+

Shipping Address

+
+
+ + 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="Apartment, suite, etc. (optional)" + 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" + /> +
+
+ + handleShippingChange("phone", e.target.value)} + className="w-full border border-border px-4 py-2 rounded" + /> +
+
+
+ + {/* Billing Address Toggle */} +
+ +
+ + {/* Billing Address (if different) */} + {!sameAsShipping && ( +
+

Billing Address

+
+
+ + handleBillingChange("firstName", e.target.value)} + className="w-full border border-border px-4 py-2 rounded" + /> +
+
+ + handleBillingChange("lastName", e.target.value)} + className="w-full border border-border px-4 py-2 rounded" + /> +
+
+ + handleBillingChange("streetAddress1", e.target.value)} + className="w-full border border-border px-4 py-2 rounded" + /> +
+
+ + handleBillingChange("city", e.target.value)} + className="w-full border border-border px-4 py-2 rounded" + /> +
+
+ + handleBillingChange("postalCode", e.target.value)} + className="w-full border border-border px-4 py-2 rounded" + /> +
+
+ + handleBillingChange("phone", e.target.value)} + className="w-full border border-border px-4 py-2 rounded" + /> +
+
+
+ )} + + {/* Payment Method */} +
+

Payment Method

+
+
+ + Cash on Delivery (COD) +
+

+ Pay when your order is delivered to your door. +

+
+
+ + {/* Submit Button */} + +
+
+ + {/* Order Summary */} +
+

Order Summary

+ + {lines.length === 0 ? ( +

Your cart is empty

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

{line.variant.product.name}

+

+ Qty: {line.quantity} +

+

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

+
+
+ ))} +
+ +
+
+ Subtotal + {formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)} +
+
+ Shipping + + {checkout?.shippingPrice?.gross?.amount + ? formatPrice(checkout.shippingPrice.gross.amount) + : "Calculated" + } + +
+
+ Total + {formatPrice(total)} +
+
+ + )} +
+
+
+
+ +
+ ); +}