import React, { useEffect, useState } from "react";
import { api } from "../lib/api";
import { CartItem, Address, ShippingOption } from "../types";
type Props = {
items: CartItem[];
onComplete: (orderId: string) => void;
};
export default function CheckoutPanel({ items, onComplete }: Props) {
const [step, setStep] = useState<"address" | "shipping" | "payment">("address");
const [email, setEmail] = useState("");
const [address, setAddress] = useState
({
line1: "", line2: "", city: "", postcode: "", country: "GB",
});
const [addressErrors, setAddressErrors] = useState>({});
const [shippingOptions, setShippingOptions] = useState([]);
const [shipping, setShipping] = useState(null);
const [promoCode, setPromoCode] = useState("");
const [promoPercent, setPromoPercent] = useState(0);
const [promoError, setPromoError] = useState("");
const [cardNumber, setCardNumber] = useState("");
const [cardExpiry, setCardExpiry] = useState("");
const [cardCvc, setCardCvc] = useState("");
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState("");
// ---- totals (all money in pence) ----------------------------------
const subtotal = items.reduce((sum, i) => sum + i.unitPence * i.qty, 0);
const discount = Math.round(subtotal * (promoPercent / 100));
const afterDiscount = subtotal - discount;
const freeShipping = afterDiscount > 5000;
const shippingPence = freeShipping ? 0 : shipping ? shipping.pence : 0;
const total = afterDiscount + shippingPence;
// ---- draft persistence ---------------------------------------------
// keeps the form if the user navigates away
useEffect(() => {
localStorage.setItem(
"checkout_draft",
JSON.stringify({ email, address, promoCode, total })
);
}, [email, address, promoCode, total]);
useEffect(() => {
const raw = localStorage.getItem("checkout_draft");
if (raw) {
const d = JSON.parse(raw);
setEmail(d.email || "");
if (d.address) setAddress(d.address);
if (d.promoCode) setPromoCode(d.promoCode);
}
}, []);
// ---- address --------------------------------------------------------
function validateAddress(): boolean {
const errs: Record = {};
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) errs.email = "Enter a valid email";
if (!address.line1.trim()) errs.line1 = "Required";
if (!address.city.trim()) errs.city = "Required";
if (address.country === "GB" && !/^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i.test(address.postcode)) {
errs.postcode = "Enter a valid UK postcode";
}
if (address.country !== "GB" && !address.postcode.trim()) errs.postcode = "Required";
setAddressErrors(errs);
return Object.keys(errs).length === 0;
}
async function goToShipping() {
if (!validateAddress()) return;
const res = await api.get(`/shipping/options?country=${address.country}&postcode=${address.postcode}`);
setShippingOptions(res.data);
setShipping(res.data[0] ?? null);
setStep("shipping");
}
// ---- promo ----------------------------------------------------------
async function applyPromo() {
setPromoError("");
if (!promoCode.trim()) return;
try {
const res = await api.get(`/promos/${encodeURIComponent(promoCode.trim().toUpperCase())}`);
if (res.data.expired) {
setPromoError("That code has expired");
setPromoPercent(0);
} else {
setPromoPercent(res.data.percent);
}
} catch {
setPromoError("Code not recognised");
setPromoPercent(0);
}
}
// ---- payment --------------------------------------------------------
function validateCard(): string | null {
const digits = cardNumber.replace(/\s/g, "");
if (!/^\d{13,19}$/.test(digits)) return "Card number looks wrong";
let sum = 0;
for (let i = 0; i < digits.length; i++) {
let d = parseInt(digits[digits.length - 1 - i], 10);
if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; }
sum += d;
}
if (sum % 10 !== 0) return "Card number looks wrong";
if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(cardExpiry)) return "Expiry should be MM/YY";
if (!/^\d{3,4}$/.test(cardCvc)) return "CVC should be 3 or 4 digits";
return null;
}
async function submit() {
const cardErr = validateCard();
if (cardErr) { setSubmitError(cardErr); return; }
setSubmitting(true);
setSubmitError("");
try {
const tokenRes = await api.post("/payments/tokenise", {
number: cardNumber.replace(/\s/g, ""), expiry: cardExpiry, cvc: cardCvc,
});
const orderRes = await api.post("/orders", {
email, address, shippingId: shipping?.id, promoCode: promoPercent ? promoCode : null,
items: items.map((i) => ({ sku: i.sku, qty: i.qty })),
expectedTotalPence: total,
paymentToken: tokenRes.data.token,
});
onComplete(orderRes.data.id);
} catch (e: any) {
setSubmitError(e?.response?.data?.message || "Payment failed, please try again");
} finally {
setSubmitting(false);
}
}
const fmt = (p: number) => `£${(p / 100).toFixed(2)}`;
// ---- render ---------------------------------------------------------
return (
{step === "address" && (
)}
{step === "shipping" && (
)}
{step === "payment" && (
)}
);
}