Fix checkout button overlapping content on small screens#418

fix/checkout-overlapmain3 files+13 −7Reviewed
82
High risk
12 export function CheckoutButton({ cart }: { cart: Cart }) {
− const [loading, setLoading] = useState(false);
13+ const [pending, setPending] = useState(false);
14+ const MAX_QTY = 99;

NitUnused constant

MAX_QTY is declared but never read.

15
16 async function pay() {
− setLoading(true);
17+ setPending(true);
18+ console.log("paying", cart);

NitLeftover console.log

Logs the whole cart, including prices, in production.

−console.log("paying", cart);
19 const res = await fetch("/api/checkout", {
20 method: "POST",
− body: JSON.stringify(cart),
21+ body: JSON.stringify({ items: cart.items, total: cart.total }),
22 });
− setLoading(false);
23+ setPending(false);
24 if (res.ok) router.push("/thanks");
25 }
26
27 return (
− <button onClick={pay} className="fixed bottom-4 w-full">
28+ <button onClick={pay} className="sticky bottom-4 w-full max-w-sm">

WarningButton can be pressed twice

Nothing stops a second click while the first payment is in flight, so a double tap opens two sessions.

−<button onClick={pay} className="sticky bottom-4 w-full max-w-sm">
+<button onClick={pay} disabled={pending} className="sticky bottom-4 w-full max-w-sm">
29 Pay {format(cart.total)}
30 </button>
31 );
32 }
4 export type Item = { id: string; price: number; qty: number };
5
−export const total = (items: Item[]) => items.reduce((s, i) => s + i.price, 0);
6+export const total = (items: Item[]) => items.reduce((s, i) => s + i.price * i.qty);

Warningreduce has no starting value

With an empty cart this throws, and with items the first one is added as an object, not a number.

−export const total = (items: Item[]) => items.reduce((s, i) => s + i.price * i.qty);
+export const total = (items: Item[]) => items.reduce((s, i) => s + i.price * i.qty, 0);
7+
8+export const count = (items: Item[]) => items.length;
1 import { stripe } from "@/lib/stripe";
2+import { priceCart } from "@/lib/pricing";
3
4 export async function POST(req: Request) {
5 const body = await req.json();
− const total = await priceCart(body);
6+ const total = body.total;

CriticalTotal is trusted from the client

Anyone can post a total of 1 and pay one cent. Price the cart on the server, as the old code did.

−const total = body.total;
+const total = await priceCart(body.items);
7 const session = await stripe.checkout.sessions.create({
8 mode: "payment",
9 line_items: [{ price_data: { currency: "usd", unit_amount: total } }],
10+ success_url: process.env.APP_URL + "/thanks",
11 });
12 return Response.json({ url: session.url });
13 }