Easy Stripe checkout with use-shopping-cart and Next.js 14


Recently, Vercel made a significant announcement at their fourth annual conference, unveiling Next.js 14. This latest version of the widely acclaimed React framework is a game-changer, promising enhanced speed and ease of use for developers.
You can read this also on Medium here.
In case you would like to have a look into the repo.
Next.js, renowned for its prowess in server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR), has been a go-to choice for creating dynamic, high-performance web applications.
Stripe is a comprehensive payment processing solution for businesses of all sizes. It simplifies online transactions by providing a seamless and secure payment platform. With Stripe, businesses can effortlessly accept various payment methods, including credit cards, digital wallets, and international currencies. Its robust API and extensive documentation make it a favorite among developers for integrating payment systems into web applications.
What is use-shopping-cart?
useShoppingCart is an elegant solution for managing shopping cart state and logic, specifically designed for Stripe checkout integration. Available at useshoppingcart.com, this tool expertly handles the intricacies of your cart’s details, streamlining the checkout process for a seamless user experience.
BEGINNING OUR JOURNEY WITH USE-SHOPPING-CART
Our first step is the installation of use-shopping-cart. You can easily add this package to your project using either of the following commands:
1npm install --save use-shopping-cart23or45yarn add use-shopping-cart
After installation, it’s crucial to configure the package with your Stripe account’s publishable key, which you can find in your Stripe dashboard.
In this tutorial, I’m crafting a storefront using Sanity.io, a headless CMS, where I’ve already set up a few sample products. Our initial task is to create CartProvider and wrap our app's root component with it. This is vital to ensure that our shopping cart's state is managed effectively throughout the application.
1 — Setting Up the CartProvider (/providers/CartProvider.tsx)
Now, let’s dive into the creation of the CartProvider component. Located at /providers/CartProvider.tsx, this component is vital for integrating the shopping cart functionality in our app. Here's a look at the code structure:
1"use client";23import { ReactNode } from "react";4import { CartProvider as USCProvider } from "use-shopping-cart";56export default function CartProvider({ children }: { children: ReactNode }) {7 return (8 <USCProvider9 mode="payment"10 cartMode="client-only"11 stripe={process.env.NEXT_PUBLIC_STRIPE_KEY as string}12 successUrl="http://localhost:3000"13 cancelUrl="http://localhost:3000/nowosci"14 currency="PLN"15 billingAddressCollection={true}16 shouldPersist={true}17 language="pl-PL"18 >19 {children}20 </USCProvider>21 );22}
In this setup, billingAddressCollection is a boolean flag allowing us to gather the customer's billing address, and shouldPersist enables the storage of cart data in local storage.
Next, integrate the CartProvider with the layout component. This requires wrapping the body components from the layout.ts file, effectively connecting the CartProvider (marked "use client") with the server-side layout component.
1//all your necessary imports + CartProvider23import CartProvider from "@/providers/CartProvider";45export default function RootLayout({6 children,7}: {8 children: React.ReactNode;9}) {10 return (11 <html lang="en">12 <body className={`${inter.variable} ${playfair.variable}`}>13 <CartProvider>14 <ToastProvider />15 <TopBanner />16 <Navbar />17 <ShoppingCartModal />18 {children}19 <Footer />20 </CartProvider>21 </body>22 </html>23 );24}
Moving forward, our task is to construct the ShoppingCartModal. For this project, I'm utilizing TailwindCSS and shadcn. Given that this modal will interact directly with customers, it's essential to run it client-side, as indicated by the "use client" directive. We'll be incorporating the useShoppingCart hook from "use-shopping-cart", and as an initial step, we're just extracting the cartCount for testing purposes.
Here’s a glimpse at the ShoppingCartModal component:
1"use client";23import {4 Sheet,5 SheetContent,6 SheetHeader,7 SheetTitle,8} from "@/components/ui/sheet";910import { useShoppingCart, formatCurrencyString } from "use-shopping-cart";1112const ShoppingCartModal = () => {13 // const cartCount: number = 4;14 const { cartCount } = useShoppingCart();1516 return (17 <>18 <Sheet defaultOpen>19 <SheetContent className="sm:max-w-lg w-[90vw]">20 <SheetHeader>21 <SheetTitle>Cart</SheetTitle>22 </SheetHeader>2324 <div className="h-full flex flex-col justify-between">25 <div className="mt-8 flex-1 overflow-y-auto">26 <ul className="-my-6 divide-y divide-gray-200">27 {cartCount === 0 ? (28 <h1 className="h3 py-6">You do not have any items</h1>29 ) : (30 <h1 className="h3 py-6">Hey you have some items!</h1>31 )}32 </ul>33 </div>34 </div>35 </SheetContent>36 </Sheet>37 </>38 );39};4041export default ShoppingCartModal;
Upon refreshing, we should see the modal open, displaying a message that reflects the current state of our cart.

2 — Enhancing the ShoppingCartModal with Additional Features
Now, let’s enrich our ShoppingCartModal by incorporating more properties and methods from useShoppingCart. This step will help us complete the modal with full functionality. Below is the updated code, including a comprehensive list of properties and their application:
1"use client";23import axios from "axios";4import { toast } from "react-hot-toast";5import { Button } from "@/components/ui/button";6import { Input } from "@/components/ui/input";78import Image from "next/image";910import {11 Sheet,12 SheetContent,13 SheetHeader,14 SheetTitle,15} from "@/components/ui/sheet";16import { useShoppingCart, formatCurrencyString } from "use-shopping-cart";17import { on } from "events";1819const ShoppingCartModal = () => {2021 const {22 cartCount,23 shouldDisplayCart,24 handleCartClick,25 cartDetails,26 removeItem,27 totalPrice,28 redirectToCheckout,29 } = useShoppingCart();303132 const items = Object.values(cartDetails ?? {}).map((entry) => entry.price_id);3334 async function handleCheckoutClick(event: any) {35 event.preventDefault();3637 try {38 const result = await redirectToCheckout();39 if (result?.error) {40 console.log("result");41 }42 } catch (err: any) {43 console.log(err.message);44 }45 }4647 return (48 <>49 <Sheet open={shouldDisplayCart} onOpenChange={() => handleCartClick()}>50 <SheetContent className="sm:max-w-lg w-[90vw]">51 <SheetHeader>52 <SheetTitle>Cart</SheetTitle>53 </SheetHeader>5455 <div className="h-full flex flex-col justify-between">56 <div className="mt-8 flex-1 overflow-y-auto">57 <ul className="-my-6 divide-y divide-gray-200">58 {cartCount === 0 ? (59 <h1 className="py-6">You do not have any items</h1>60 ) : (61 <>62 {Object.values(cartDetails ?? {}).map((entry) => (63 <li key={entry.id} className="flex py-6 ">64 <div className="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200">65 <Image66 src={entry.image as string}67 alt="Product Image"68 width={100}69 height={100}70 />71 </div>7273 <div className="ml-4 flex flex-1 flex-col">74 <div>75 <div className="flex justify-between text-base font-medium text-gray-900">76 <h3>{entry.name}</h3>77 <p className="ml-4">{entry.price} PLN</p>78 <p className="mt-1 text-sm text-gray-500 line-clamp-2">79 {entry.description}80 </p>81 </div>8283 <div className="flex flex-1 items-end justify-between text-sm">84 <p className="text-gray-500">85 QTY: {entry.quantity}86 </p>8788 <div className="flex">89 <button90 onClick={() => removeItem(entry.id)}91 type="button"92 className="font-medium text-primary hover:text-primary/80"93 >94 Remove95 </button>96 </div>97 </div>98 </div>99 </div>100 </li>101 ))}102 </>103 )}104 </ul>105 </div>106 <div className="border-t border-gray-200 px-4 py-6 sm:px-6">107 <div className="flex justify-between text-base font-medium text-gray-900">108 <p>Subtotal</p>109 <p>{totalPrice} PLN</p>110 </div>111 <p className="mt-0.5 text-sm text-gray-500">112 Shipping and taxes are calculated at checkout113 </p>114115 <div className="mt-6">116 <Button117 onClick={handleCheckoutClick}118 className="w-full bg-black text-white"119 >120 Pay now121 </Button>122 </div>123124 <div className="mt-6 flex justify-center text-center text-sm text-gray-500">125 <Button className="w-full mt-4">Continue shopping</Button>126 </div>127 </div>128 </div>129 </SheetContent>130 </Sheet>131 </>132 );133};134135export default ShoppingCartModal;
This enhanced modal leverages the shouldDisplayCart property and handleCartClick() method from useShoppingCart to manage its display state:
1<Sheet open={shouldDisplayCart} onOpenChange={() => handleCartClick()}>
To trigger the cart modal, consider updating components like your NavBar or any other area in your app where you’d like to provide access to the Cart.
1"use client";23import Link from "next/link";4import { usePathname } from "next/navigation";5import { Button } from "@/components/ui/button";6import { Heart, Menu, Search, ShoppingBag, User } from "lucide-react";7import NavbarMobile from "./NavbarMobile";89// import useCart from "@/hooks/use-cart";10import { useShoppingCart } from "use-shopping-cart";1112import { useRouter } from "next/navigation";1314const links = [...];1516const Navbar = () => {17 const pathname = usePathname();18 const router = useRouter();1920 // destructuring to get handleCartClick()21 const { handleCartClick, cartCount } = useShoppingCart();2223 return (24 <header className="mt-2 mb-2">25 <div className="flex items-center justify-between mx-auto max-w-2xl px-4 sm:px-6 lg:max-w-7xl">26 <Link href="/">27 <h1 className="text-2xl md:text-4xl font-playfair font-bold">28 Gibbarosa29 </h1>30 </Link>3132 <nav className="hidden gap-12 lg:flex 2xl:ml-16 mt-2">33 {links.map((link, idx) => (34 <div key={idx}>35 {pathname === link.href ? (36 <Link37 className="text-[14px] text-black underline"38 href={link.href}39 >40 {link.name}41 </Link>42 ) : (43 <Link44 href={link.href}45 className="text-[14px] text-gray-600 transition duration-100 underline-effect"46 >47 {link.name}48 </Link>49 )}50 </div>51 ))}52 </nav>5354 <div className="flex items-center">55 <Button className="bg-transparent text-black">56 <Search size={18} />57 </Button>58 <Button className="bg-transparent text-black">59 <User size={18} />60 </Button>61 <Button className="bg-transparent text-black">62 <Heart size={18} />63 </Button>6465 {/* destructuring to get handleCartClick() */}6667 <Button68 onClick={() => handleCartClick()}69 className="bg-transparent text-black"70 >71 <ShoppingBag size={18} />72 <span>{cartCount}</span>73 </Button>74 </div>7576 {/* Mobile Navbar */}77 <NavbarMobile />78 </div>79 </header>80 );81};8283export default Navbar;
3 — Crafting the AddToCart Component with useShoppingCart
The next step in our journey is to develop the AddToCart component. This component will utilize useShoppingCart to handle a variety of product attributes such as name, currency, description, price, image, and the price_id (which is generated by Stripe when a product is added to the Stripe dashboard).
1"use client";23import { Button } from "@/components/ui/button";4import { useShoppingCart } from "use-shopping-cart";5import { urlFor } from "@/lib/sanity";67export interface ProductCart {8 name: string;9 description: string;10 price: number;11 currency: string;12 image: any;13 price_id: string;14}1516const AddToCart = ({17 name,18 currency,19 description,20 price,21 image,22 price_id,23}: ProductCart) => {24 const { addItem, handleCartClick } = useShoppingCart();2526 const product = {27 name: name,28 description: description,29 price: price,30 currency: currency,31 image: urlFor(image).url(),32 price_id: price_id,33 };3435 return (36 <div>37 <Button38 onClick={() => {39 addItem(product), handleCartClick();40 }}41 className="bg-black text-white w-full"42 >43 Add to Cart44 </Button>45 </div>46 );47};4849export default AddToCart;
This component simplifies the process of adding products to the shopping cart. It’s designed to be integrated into product pages, like app/product/[slug], where it can accept product properties directly.
As a note, I’m leveraging Sanity.io, a headless CMS, for building the storefront. This approach involves using groq queries to import product data based on the Sanity-generated slug.
1import Image from "next/image";2import { client } from "@/lib/sanity";3import { fullProduct } from "@/interface";4import ImageGallery2 from "@/components/ImageGallery2";5import { Button } from "@/components/ui/button";67import {8 Accordion,9 AccordionContent,10 AccordionItem,11 AccordionTrigger,12} from "@/components/ui/accordion";1314import AddToCart from "@/components/AddToCart";1516import MidBanner from "@/components/MidBanner";17import Newsletter from "@/components/Newsletter";18import YouMayLike from "@/components/YouMayLike";1920async function getData(slug: string) {21 const query = `*[_type == "product" && slug.current == "${slug}"][0] {22 _id,23 images,24 name,25 brand,26 price,27 condition,28 size,29 description,30 tags,31 "slug": slug.current,32 "categoryName": category -> name,33 price_id,3435 } `;3637 const data = await client.fetch(query);3839 return data;40}4142const ProductPage = async ({ params }: { params: { slug: string } }) => {43 const data: fullProduct = await getData(params.slug);4445 return (46 <section className="mt-20 mx-auto max-w-2xl px-4 sm:pb-6 lg:max-w-7xl lg:px-8">47 <div className="grid gap-8 md:grid-cols-2">48 <ImageGallery2 images={data.images} />4950 <div className="flex flex-col">51 {/* KEY INFO */}52 <div>53 <h1 className="h4">{data.brand}</h1>54 <h1 className="h2">{data.name}</h1>55 <p className="mt-4">{data.description}</p>56 <h1 className="mt-6 text-3xl font-bold">{data.price} PLN</h1>57 </div>5859 {/* SIZES */}60 <div className="mt-4 mb-4">Size:</div>6162 {/* OTHER */}6364 <div className="mt-6 flex space-x-8">65 <div className="flex-col">66 <div className="mb-2 text-sm uppercase text-gray-400">Condition</div>67 <div className="mb-2 text-sm uppercase text-gray-400">68 Size69 </div>70 <div className="mb-2 text-sm uppercase text-gray-400">71 Accessories72 </div>73 </div>74 <div className="flex-col">75 <div className="mb-2 text-sm capitalize">{data.condition}</div>7677 </div>78 </div>7980 {/* ADD TO CART BUTTON */}81 <AddToCart82 key={data._id}83 currency="PLN"84 description={data.description}85 image={data.images[0]}86 name={data.name}87 price={data.price}88 price_id={data.price_id}89 />9091 {/* PRODUCT ACCORDION */}92 <Accordion93 type="single"94 collapsible95 defaultValue="item-1"96 className="w-full"97 >98 .....99 </Accordion>100 </div>101 </div>102103 <YouMayLike />104 <MidBanner />105 <Newsletter />106 </section>107 );108};109110export default ProductPage;
4- Check the below screens on how to add product to Stripe and get price_id.


It’s important to activate the ‘client-only integration’ feature in your Stripe dashboard. You can do this by visiting Stripe’s Checkout Settings. This setting is crucial for the proper functioning of the integration with your application.

Here’s what you can expect to see once a product has been successfully added to the cart. This view provides a clear indication that the item has been integrated into your shopping cart effectively.

CONCLUSION: Navigating the Integration of Stripe Checkout and use-shopping-cart
I attempted to guide you through the process of integrating Stripe checkout with use-shopping-cart in a Next.js 14 framework. Our journey began with the initial setup of use-shopping-cart, followed by the intricate configuration of the CartProvider. We then delved into creating the ShoppingCartModal and the AddToCart component, ensuring a fluid user interaction with the shopping cart.
Happy coding :)
Piotr

