import { useState, useEffect } from "react"; import { useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; import { useCart } from "../hooks/use-cart"; import { apiRequest } from "../lib/queryClient"; import { toast } from "../hooks/use-toast"; import { Shield, Lock, CreditCard } from "lucide-react"; declare global { interface Window { Accept: { dispatchData: (secureData: any, callback: (response: any) => void) => void; }; } } interface StoreConfig { authorizeNetClientKey: string; authorizeNetApiLoginId: string; authorizeNetSandbox: boolean; stripePublishableKey?: string; stripeEnabled: boolean; } interface Address { firstName: string; lastName: string; address1: string; address2?: string; city: string; state: string; zip: string; country: string; } const US_STATES = [ "AL","AK","AZ","AR","CA","CO","CT","DE","FL","GA","HI","ID","IL","IN","IA", "KS","KY","LA","ME","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ", "NM","NY","NC","ND","OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VT", "VA","WA","WV","WI","WY","DC", ]; function AddressForm({ prefix, value, onChange }: { prefix: string; value: Address; onChange: (v: Address) => void; }) { const field = (name: keyof Address, label: string, required = true, type = "text") => (
onChange({ ...value, [name]: e.target.value })} required={required} className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" data-testid={`input-${prefix}-${name}`} />
); return (
{field("firstName", "First name")} {field("lastName", "Last name")}
{field("address1", "Address")}
{field("address2", "Apt, suite, etc.", false)}
{field("city", "City")}
{field("zip", "ZIP code")}
); } export default function CheckoutPage() { const { items, subtotal, isLoading: cartLoading } = useCart(); const [, setLocation] = useLocation(); const [step, setStep] = useState<"info" | "payment">("info"); const [submitting, setSubmitting] = useState(false); const [processor, setProcessor] = useState<"authorizenet" | "stripe">("authorizenet"); const [email, setEmail] = useState(""); const [shipping, setShipping] = useState
({ firstName: "", lastName: "", address1: "", city: "", state: "", zip: "", country: "US" }); const [sameAsBilling, setSameAsBilling] = useState(true); const [billing, setBilling] = useState
({ firstName: "", lastName: "", address1: "", city: "", state: "", zip: "", country: "US" }); const [cardNumber, setCardNumber] = useState(""); const [expiry, setExpiry] = useState(""); const [cvv, setCvv] = useState(""); const [discountCode, setDiscountCode] = useState(""); const [discountApplied, setDiscountApplied] = useState<{ code: string; type: string; value: string } | null>(null); const [applyingDiscount, setApplyingDiscount] = useState(false); const { data: config } = useQuery({ queryKey: ["/api/config"], queryFn: async () => { const r = await fetch("/api/config"); return r.json(); }, }); useEffect(() => { if (config && !config.stripeEnabled) setProcessor("authorizenet"); }, [config]); useEffect(() => { if (config?.authorizeNetSandbox === false) { const existing = document.getElementById("anet-js"); if (!existing) { const script = document.createElement("script"); script.id = "anet-js"; script.src = "https://js.authorize.net/v1/Accept.js"; script.async = true; document.head.appendChild(script); } } else if (config) { const existing = document.getElementById("anet-js"); if (!existing) { const script = document.createElement("script"); script.id = "anet-js"; script.src = "https://jstest.authorize.net/v1/Accept.js"; script.async = true; document.head.appendChild(script); } } }, [config]); if (cartLoading) return
Loading...
; if (items.length === 0) { setLocation("/"); return null; } const calcDiscount = () => { if (!discountApplied) return 0; if (discountApplied.type === "percentage") return subtotal * (parseFloat(discountApplied.value) / 100); return Math.min(parseFloat(discountApplied.value), subtotal); }; const discount = calcDiscount(); const total = Math.max(0, subtotal - discount); const handleApplyDiscount = async () => { if (!discountCode.trim()) return; setApplyingDiscount(true); try { const res = await apiRequest("POST", "/api/cart/discount", { code: discountCode.trim() }); const data = await res.json(); setDiscountApplied(data.discount); toast({ title: "Discount applied!", description: `${data.discount.code} is active.` }); } catch (err: any) { toast({ title: "Invalid code", description: err.message, variant: "destructive" }); } finally { setApplyingDiscount(false); } }; const handleAuthorizeNetSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!config?.authorizeNetApiLoginId || !config?.authorizeNetClientKey) { toast({ title: "Payment not configured", variant: "destructive" }); return; } setSubmitting(true); const secureData = { authData: { clientKey: config.authorizeNetClientKey, apiLoginID: config.authorizeNetApiLoginId }, cardData: { cardNumber: cardNumber.replace(/\s/g, ""), month: expiry.split("/")[0]?.trim(), year: `20${expiry.split("/")[1]?.trim()}`, cardCode: cvv, }, }; window.Accept.dispatchData(secureData, async (response) => { if (response.messages.resultCode === "Error") { toast({ title: "Card error", description: response.messages.message[0]?.text, variant: "destructive" }); setSubmitting(false); return; } try { const res = await apiRequest("POST", "/api/checkout/authorizenet", { dataDescriptor: response.opaqueData.dataDescriptor, dataValue: response.opaqueData.dataValue, email, firstName: shipping.firstName, lastName: shipping.lastName, shippingAddress: shipping, billingAddress: sameAsBilling ? shipping : billing, }); const data = await res.json(); setLocation(`/order-confirmation?order=${data.orderNumber}`); } catch (err: any) { toast({ title: "Payment failed", description: err.message, variant: "destructive" }); setSubmitting(false); } }); }; return (

Checkout

{step === "info" && (
{ e.preventDefault(); setStep("payment"); }} className="space-y-6">

Contact

setEmail(e.target.value)} required className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" data-testid="input-email" />

Shipping address

Discount code

setDiscountCode(e.target.value)} placeholder="Enter code" className="flex-1 px-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" data-testid="input-discount-code" />
{discountApplied && (

✓ {discountApplied.code} applied — {discountApplied.type === "percentage" ? `${discountApplied.value}% off` : `$${discountApplied.value} off`}

)}
)} {step === "payment" && (
{config?.stripeEnabled && (

Payment method

)} {processor === "authorizenet" && (

Card details

Secure
setCardNumber(e.target.value.replace(/\D/g, "").replace(/(.{4})/g, "$1 ").trim())} placeholder="1234 5678 9012 3456" maxLength={19} required className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background font-mono focus:outline-none focus:ring-2 focus:ring-primary/30" data-testid="input-card-number" />
{ let v = e.target.value.replace(/\D/g, ""); if (v.length >= 3) v = v.slice(0, 2) + " / " + v.slice(2, 4); setExpiry(v); }} placeholder="MM / YY" maxLength={7} required className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background font-mono focus:outline-none focus:ring-2 focus:ring-primary/30" data-testid="input-expiry" />
setCvv(e.target.value.replace(/\D/g, "").slice(0, 4))} placeholder="CVV" maxLength={4} required className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background font-mono focus:outline-none focus:ring-2 focus:ring-primary/30" data-testid="input-cvv" />
)}

Billing address

{!sameAsBilling && }
)}

Order summary

{items.map((item) => (
{item.imageUrl && {item.title}}
{item.quantity}

{item.title}

{item.variantTitle && item.variantTitle !== "Default Title" &&

{item.variantTitle}

}
${(parseFloat(item.price) * item.quantity).toFixed(2)}
))}
Subtotal ${subtotal.toFixed(2)}
{discount > 0 && (
Discount -${discount.toFixed(2)}
)}
Shipping Calculated at next step
Total ${total.toFixed(2)}
); }