Skip to content

Commit

Permalink
Implement checkout with chosen cart items
Browse files Browse the repository at this point in the history
  • Loading branch information
lhvy committed Jun 18, 2024
1 parent ef02d0e commit 110d2a4
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 28 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"swiper": "11.1.0",
"typescript": "5.4.5",
"typewriter-effect": "2.21.0",
"wnumb": "1.2.0"
"wnumb": "1.2.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/js": "9.4.0",
Expand Down
55 changes: 40 additions & 15 deletions pages/api/checkout_sessions.js → pages/api/checkout_sessions.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable no-undef */

// From https://docs.stripe.com/checkout/embedded/quickstart?client=next

import { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";

const itemSchema = z.object({
price: z.string(),
quantity: z.number().int(),
});

const bodySchema = z.object({
items: z.array(itemSchema),
});

type Body = z.infer<typeof bodySchema>;

type ResponseData = {
error?: string;
clientSecret?: string;
status?: string;
customer_email?: string;
};

// TODO update secret key in .env.local
// TODO (opt) - change from CJS/require to MJS/loadStripe - see checkout/index.tsx
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

export default async function handler(req, res) {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>,
) {
switch (req.method) {
case "POST":
try {
const result = bodySchema.safeParse(JSON.parse(req.body));
if (!result.success) {
return res
.status(400)
.json({ error: "Invalid request body. Please try again." });
}
const validBody: Body = result.data;

// Create Checkout Sessions from body params.
const session = await stripe.checkout.sessions.create({
ui_mode: "embedded",
line_items: [
{
// Provide the exact Price ID (for example, pr_1234) of
// the product you want to sell
price: "price_1PQoBSKWz42bhxUEodZ18Z6I",
quantity: 1,
// TODO - get info from cart / price IDs
// (could get the price for the given product with stripe API)
},
],
line_items: validBody.items,
mode: "payment",
return_url: `${req.headers.origin}/checkout/return?session_id={CHECKOUT_SESSION_ID}`,
});

res.send({ clientSecret: session.client_secret });
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
res.status(err.statusCode || 500).json(err.message);
}
break;
Expand All @@ -43,12 +66,14 @@ export default async function handler(req, res) {
status: session.status,
customer_email: session.customer_details.email,
});
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
res.status(err.statusCode || 500).json(err.message);
}
break;
default:
res.setHeader("Allow", req.method);
// TODO: Do we need this?
// res.setHeader("Allow", req.method);
res.status(405).end("Method Not Allowed");
}
}
45 changes: 35 additions & 10 deletions pages/checkout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,57 @@ import {
EmbeddedCheckoutProvider,
} from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import { useCallback } from "react";
import { CartItemWithDetail } from "api/merch";
import { useCallback, useEffect, useState } from "react";

const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || "NO_KEY_FOUND",
);

// TODO: Do not display the checkout unless client secret is fetched, and cart is not empty
const Checkout = () => {
const [props, setProps] = useState<CartItemWithDetail[]>([]);

useEffect(() => {
const cart = localStorage.getItem("cart");
let items: CartItemWithDetail[] = [];
if (cart !== null) {
items = JSON.parse(cart);
} else {
items = [];
}
setProps(items);
}, []);

const fetchClientSecret = useCallback(async () => {
// Create a Checkout Session
const res = await fetch("/api/checkout_sessions", {
method: "POST",
body: JSON.stringify({
items: props.map((item) => ({
price: item.price.id,
quantity: item.qty,
})),
}),
});
const data = await res.json();
return data.clientSecret;
}, []);
}, [props]);

if (props.length) {
const options = { fetchClientSecret };

const options = { fetchClientSecret };
return (
<div id="checkout" className="mb-4 mb-xl-5">
<EmbeddedCheckoutProvider stripe={stripePromise} options={options}>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</div>
);
}

return (
<div id="checkout" className="mb-4 mb-xl-5">
<EmbeddedCheckoutProvider stripe={stripePromise} options={options}>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</div>
);
// TODO: Improve this case
return <div>Cart is empty</div>;
};

export default Checkout;
25 changes: 23 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3991,7 +3991,14 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1", [email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

[email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand Down Expand Up @@ -4574,7 +4581,16 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", [email protected], wrap-ansi@^8.1.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

[email protected], wrap-ansi@^8.1.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand All @@ -4598,6 +4614,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==

zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==

zwitch@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
Expand Down

0 comments on commit 110d2a4

Please sign in to comment.