diff --git a/client/src/pages/checkout.tsx b/client/src/pages/checkout.tsx
new file mode 100644
index 0000000..01a41f1
--- /dev/null
+++ b/client/src/pages/checkout.tsx
@@ -0,0 +1,389 @@
+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" && (
+
+ )}
+
+ {step === "payment" && (
+
+ )}
+
+
+
+
+
Order summary
+
+ {items.map((item) => (
+
+
+
+ {item.imageUrl &&

}
+
+
{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)}
+
+
+
+
+
+
+ );
+}