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