From e2afdb20c181bc75060b4abd24d27b50235e6cae Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Sat, 1 Aug 2026 16:26:36 +0000 Subject: [PATCH] Deploy --- src/app/checkout/page.tsx | 207 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 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..282bc69 --- /dev/null +++ b/src/app/checkout/page.tsx @@ -0,0 +1,207 @@ +'use client' +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { ArrowLeft, CircleCheck, Loader2 } from 'lucide-react' +import { medusa } from '@/lib/medusa' +import { broadcastCartCount } from '@/hooks/useMedusaCart' + +const CURRENCY_SYMBOLS: Record = { zar: 'R', usd: '$', eur: '€', gbp: '£', kes: 'KSh', ngn: '₦', ghs: '₵' } +const formatPrice = (amount: number, code?: string) => { + const sym = CURRENCY_SYMBOLS[(code ?? '').toLowerCase()] ?? (code?.toUpperCase() ?? '') + return `${sym}${(amount / 100).toFixed(2)}` +} + +type Step = 'details' | 'review' | 'success' +interface Addr { first_name: string; last_name: string; address_1: string; city: string; country_code: string; postal_code: string; phone: string } +interface CartItem { id: string; title: string; quantity: number; unit_price: number; thumbnail: string | null; variant: { title: string } | null } +interface Cart { id: string; items: CartItem[]; subtotal: number; region: { currency_code: string } | null } + +export default function CheckoutPage() { + const router = useRouter() + const [step, setStep] = useState('details') + const [cart, setCart] = useState(null) + const [loading, setLoading] = useState(true) + const [placing, setPlacing] = useState(false) + const [orderError, setOrderError] = useState(null) + const [email, setEmail] = useState('') + const [addr, setAddr] = useState({ first_name: '', last_name: '', address_1: '', city: '', country_code: 'za', postal_code: '', phone: '' }) + + const cartId = typeof window !== 'undefined' ? localStorage.getItem('medusa_cart_id') : null + + useEffect(() => { + if (!cartId) { setLoading(false); return } + medusa.store.cart.retrieve(cartId) + .then(({ cart: c }) => setCart(c as Cart)) + .catch(() => setCart(null)) + .finally(() => setLoading(false)) + }, [cartId]) + + const handleDetailsSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!cartId) return + try { + await (medusa.store.cart.update as Function)(cartId, { email, shipping_address: addr }) + } catch {} + setStep('review') + } + + const handlePlaceOrder = async () => { + setPlacing(true) + setOrderError(null) + try { + if (!cartId) return + + const pubKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' + const h = { 'Content-Type': 'application/json', 'x-publishable-api-key': pubKey } + + // Step 1: find first available payment provider for this cart's region. + // Default to pp_system_default (Medusa v2 built-in manual provider) — always + // available when provisioning has run. pp_test_test does not exist. + let providerId = 'pp_system_default' + try { + const cartRes = await fetch(`/api/medusa/store/carts/${cartId}`, { headers: h }) + const { cart: cartData } = await cartRes.json() + if (cartData?.region_id) { + const provRes = await fetch(`/api/medusa/store/payment-providers?region_id=${cartData.region_id}`, { headers: h }) + const provData = await provRes.json() + const first = provData?.payment_providers?.[0] + if (first?.id) providerId = first.id + } + } catch {} + + // Step 2: create payment collection (Medusa v2 — must come before session) + const pcRes = await fetch('/api/medusa/store/payment-collections', { + method: 'POST', headers: h, body: JSON.stringify({ cart_id: cartId }), + }) + if (!pcRes.ok) throw new Error(`Payment setup failed (${pcRes.status})`) + const { payment_collection } = await pcRes.json() + + // Step 3: create payment session on the collection + const psRes = await fetch(`/api/medusa/store/payment-collections/${payment_collection.id}/payment-sessions`, { + method: 'POST', headers: h, body: JSON.stringify({ provider_id: providerId }), + }) + if (!psRes.ok) throw new Error(`Payment session failed (${psRes.status})`) + + // Step 4: complete cart — creates the order in Medusa + const result = await (medusa.store.cart.complete as Function)(cartId) + const order = result?.order ?? result + localStorage.removeItem('medusa_cart_id') + localStorage.removeItem('medusa_cart_count') + broadcastCartCount(0) + if (order?.id) { + router.push(`/order/${order.id}`) + return + } + // Completed without an order id — treat as success (e.g. pending payment) + setStep('success') + } catch (err: any) { + console.error('Checkout error:', err) + setOrderError(err?.message || 'Something went wrong. Please try again.') + } finally { + setPlacing(false) + } + } + + const currency = cart?.region?.currency_code?.toUpperCase() ?? 'ZAR' + const items = cart?.items ?? [] + const subtotal = cart?.subtotal ?? 0 + + if (loading) return
+ + if (step === 'success') return ( +
+
+ +

Order Placed!

+

Thank you{email ? ', ' + email.split('@')[0] : ''}. Your order has been received.

+ Continue Shopping +
+
+ ) + + return ( +
+
+
+ Back to Cart +

Checkout

+
+
+ 1. Your Details + + 2. Review {'&'} Confirm +
+
+
+ {step === 'details' && ( +
+
+

Contact

+ setEmail(e.target.value)} placeholder="Email address" + className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary" /> +
+
+

Shipping Address

+
+ {([['first_name','First name',false],['last_name','Last name',false],['address_1','Street address',true],['city','City',false],['postal_code','Postal code',false],['phone','Phone (optional)',true]] as [keyof Addr, string, boolean][]).map(([key, label, full]) => ( + setAddr(p => ({ ...p, [key]: e.target.value }))} placeholder={label} + required={key !== 'phone'} className={(full ? 'col-span-2 ' : '') + 'border border-border rounded-lg px-3 py-2 text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary'} /> + ))} +
+
+ +
+ )} + {step === 'review' && ( +
+
+
+

Delivery Details

+ +
+

{email}

+

{addr.first_name} {addr.last_name}, {addr.address_1}, {addr.city} {addr.postal_code}

+
+ {orderError && ( +
+ {orderError} +
+ )} + +

Your order is confirmed and payment processed securely.

+
+ )} +
+
+
+

Order Summary

+
+ {items.map(item => ( +
+ {item.thumbnail && {(item} +
+

{(item as any).product_title || item.title}

+

Qty: {item.quantity}

+
+

{formatPrice(item.unit_price * item.quantity, cart?.region?.currency_code)}

+
+ ))} +
+
+
Subtotal{formatPrice(subtotal, cart?.region?.currency_code)}
+
ShippingTBD
+
+
+ Total{formatPrice(subtotal, cart?.region?.currency_code)} +
+
+
+
+
+
+ ) +}