/use-shopping-cart

Shopping cart state and logic for Stripe

Primary LanguageJavaScript

https://useshoppingcart.com

use-shopping-cart

All Contributors

A React Hook that handles shopping cart state and logic for Stripe.

NPM JavaScript Style Guide

All Contributors

Installation

# With Yarn
yarn add @stripe/stripe-js use-shopping-cart

# With NPM
npm install --save @stripe/stripe-js use-shopping-cart

Usage

Initialization

At the root level of your application (or the highest point you'll be using Stripe from), wrap your components in a <CartProvider>.

<CartProvider> comes with several props that allow you to interact with the Stripe API and customize the Stripe experience.

When loading up Stripe, don't forget to use your public Stripe API key with it. If you need help setting up your environment variables for this, view a list of environment variable tutorials.

Creating a CheckoutSession server-side allows for a more flexible and powerful integration but requires a server component (e.g. a Netlify Function).

At the root level of your app, wrap your Root app in the <CartProvider />

import ReactDOM from 'react-dom'

import { loadStripe } from '@stripe/stripe-js'
import { CartProvider } from 'use-shopping-cart'

import App from './App'

// Remember to add your public Stripe key
const stripePromise = loadStripe(process.env.STRIPE_API_PUBLIC)

ReactDOM.render(
  <CartProvider mode="checkout-session" stripe={stripePromise} currency="USD">
    <App />
  </CartProvider>,
  document.getElementById('root')
)

When using CheckoutSessions your product object must adhere to the following shape:

const products = [
  {
    // Line item name to be shown on the Stripe Checkout page
    name: 'Bananas',
    // Optional description to be shown on the Stripe Checkout page
    description: 'Yummy yellow fruit',
    // A unique identifier for this item (stock keeping unit)
    sku: 'sku_banana001',
    // price in smallest currency unit (e.g. cent for USD)
    price: 400,
    currency: 'USD',
    // Optional image to be shown on the Stripe Checkout page
    image: 'https://my-image.com/image.jpg'
  }
  /* ... more products */
]

Additionally, you must verify the cartItems on the server-side before creating the CheckoutSession. For this you can use the validateCartItems() helper.

To operate a checkout page without any server component you need to enable client-only checkout mode and insert your product information in your Stripe Dashboard:

At the root level of your app, wrap your Root app in the <CartProvider />

import ReactDOM from 'react-dom'

import { loadStripe } from '@stripe/stripe-js'
import { CartProvider } from 'use-shopping-cart'

import App from './App'

// Remember to add your public Stripe key
const stripePromise = loadStripe(process.env.STRIPE_API_PUBLIC)

ReactDOM.render(
  <CartProvider
    mode="client-only"
    stripe={stripePromise}
    // The URL to which Stripe should send customers when payment is complete.
    successUrl="http://localhost:3000/success"
    // The URL to which Stripe should send customers when payment is canceled.
    cancelUrl="http://localhost:3000"
    currency="USD"
    // https://stripe.com/docs/payments/checkout/client#collect-shipping-address
    allowedCountries={['US', 'GB', 'CA']}
    // https://stripe.com/docs/payments/checkout/client#collect-billing-address
    billingAddressCollection={true}
  >
    <App />
  </CartProvider>,
  document.getElementById('root')
)

When operating in client-only mode you must set the successUrl and cancelUrl props on the CartProvider component, and the product object must adhere to the following shape:

const products = [
  {
    name: 'Bananas',
    // sku ID from your Stripe Dashboard
    sku: 'sku_GBJ2Ep8246qeeT',
    // price in smallest currency unit (e.g. cent for USD)
    price: 400,
    currency: 'USD',
    // Optional image to be shown on the Stripe Checkout page
    image: 'https://my-image.com/image.jpg'
  }
  /* ... more products */
]

Using the hook

The hook useShoppingCart() provides several utilities and pieces of data for you to use in your application. The examples below won't cover every part of the useShoppingCart() API but you can look at the API below.

import { useShoppingCart } from 'use-shopping-cart'
import { Product } from './Product'
import { CartItems } from './CartItems'

const productData = [
  {
    name: 'Bananas',
    sku: 'sku_GBJ2Ep8246qeeT',
    price: 400,
    image: 'https://www.fillmurray.com/300/300',
    currency: 'USD'
  },
  {
    name: 'Tangerines',
    sku: 'sku_GBJ2WWfMaGNC2Z',
    price: 100,
    image: 'https://www.fillmurray.com/300/300',
    currency: 'USD'
  }
]

export function App() {
  /* Gets the totalPrice and a method for redirecting to stripe */
  const { totalPrice, redirectToCheckout, cartCount } = useShoppingCart()

  return (
    <div>
      {/* Renders the products */}
      {productData.map((product) => (
        <Product key={product.sku} product={product} />
      ))}

      {/* This is where we'll render our cart */}
      <p>Number of Items: {cartCount}</p>
      <p>Total: {totalPrice()}</p>
      <CartItems />

      {/* Redirects the user to Stripe */}
      <button onClick={() => redirectToCheckout()}>Checkout</button>
    </div>
  )
}

How do I add an item to the user's cart?

To add a product to the cart, use useShoppingCart()'s addItem(product) method. It takes in your product object, which must have a sku and a price, and adds it to the cart.

import { useShoppingCart, formatCurrencyString } from 'use-shopping-cart'

export function Product({ product }) {
  const { addItem } = useShoppingCart()

  /* A helper function that turns the price into a readable format */
  const price = formatCurrencyString({
    value: product.price,
    currency: product.currency,
    language: navigator.language
  })

  return (
    <article>
      <figure>
        <img src={product.image} alt={`Image of ${product.name}`} />
        <figcaption>{product.name}</figcaption>
      </figure>
      <p>{price}</p>

      {/* Adds the item to the cart */}
      <button
        onClick={() => addItem(product)}
        aria-label={`Add ${product.name} to your cart`}
      >
        Add to cart
      </button>
    </article>
  )
}

Now how do I display the cart to the user?

Once the user has added their items to the cart, you can use the cartDetails object to display the different data about each product in their cart.

Each product in cartDetails contains the same data you provided when you called addItem(product). In addition, cartDetails also provides the following properties:

Name Value
quantity Number of that product added to the cart
value The price * quantity
formattedValue A currency formatted version of value
import { useShoppingCart } from 'use-shopping-cart'

export function CartItems() {
  const {
    cartDetails,
    decrementItem,
    incrementItem,
    removeItem
  } = useShoppingCart()

  const cart = []
  // Note: Object.keys().map() takes 2x as long as a for-in loop
  for (const sku in cartDetails) {
    const cartEntry = cartDetails[sku]

    // all of your basic product data still exists (i.e. name, image, price)
    cart.push(
      <article>
        {/* image here */}
        {/* name here */}
        {/* formatted total price of that item */}
        <p>Line total: {cartEntry.formattedValue}</p>

        {/* What if we want to remove one of the item... or add one */}
        <button
          onClick={() => decrementItem(cartEntry.sku)}
          aria-label={`Remove one ${cartEntry.name} from your cart`}
        >
          -
        </button>
        <p>Quantity: {cartEntry.quantity}</p>
        <button
          onClick={() => incrementItem(cartEntry.sku)}
          aria-label={`Add one ${cartEntry.name} to your cart`}
        >
          +
        </button>

        {/* What if we don't want this product at all */}
        <button
          onClick={() => removeItem(cartEntry.sku)}
          aria-label={`Remove all ${cartEntry.name} from your cart`}
        >
          Remove
        </button>
      </article>
    )
  }

  return cart
}

Note that in the above code, to reduce the quantity of a product in the user's cart, you must pass an SKU to decrementItem() like so (note that you can also decrease by more than one):

decrementItem(cartEntry.sku)

// or decrease by a count
decrementItem(cartEntry.sku, 3)

This also works the same when trying to increase the quantity of a product:

incrementItem(cartEntry)

// increase by a count
incrementItem(cartEntry.sku, 4)

Just like you can reduce or increase the quantity of a product you can remove the product entirely with removeItem():

removeItem(cartEntry.sku)

How could the user set the quantity of an item to a specific number?

This is achievable with the setItemQuantity(sku, quantity) method (introduced in 2.0.0). It takes an SKU of a product in the cart and the quantity of that product you wish to have. Here is a very simple example:

export function GiveMeFiveDonutsPlease() {
  const { setItemQuantity } = useShoppingCart()
  return (
    <button onClick={() => setItemQuantity('donuts-1a2b3c', 5)}>
      Set donut quantity to 5
    </button>
  )
}

For a real-world robust example visit the documentation for setItemQuantity.

API

You can view the full API on our documentation page.

<CartProvider>

Props for this component in Client-only mode:

Name Type
mode "client-only"
stripe Stripe | undefined
successUrl string
cancelUrl string
currency string
language string
billingAddressCollection boolean
allowedCountries null | string[]

And now, CheckoutSession mode:

Name Type
mode "checkout-session"
stripe Stripe | undefined
currency string
language string

useShoppingCart()

Returns an object with all the following properties and methods:

Name Type/Args Return Type
addItem() product: Object N/A
incrementItem() sku: string N/A
decrementItem() sku: string N/A
removeItem() sku: string N/A
setItemQuantity() sku: string, quantity: number N/A
totalPrice N/A number
formattedTotalPrice N/A string
cartCount number N/A
cartDetails Object of cart entries N/A
redirectToCheckout() sessionId?: string Error (if one occurrs)
clearCart() N/A N/A

formatCurrencyString(options)

This function takes one options argument, these are the options for this function:

Name Type
value number
currency string
language string (optional)

Environment Variable Tutorials

The following tutorials teach how to set up your custom environment variables for your project.

License

MIT © dayhaysoos


Working on this project:

If you're working on this project don't forget to check out the CONTRIBUTING.md file.

Before you run any of the examples be sure to set your environment variables at the root of the project in a .env.development file (or documentation/.env.development for the documentation workspace). There is a .env.example and a documentation/.env.example file with the example variables you'll need to run the examples and documentation workspaces in this project. You'll need to fill them in with your own API keys from Stripe.

# .env.example
STRIPE_API_PUBLIC=
STRIPE_API_SECRET=

# documentation/.env.example
GATSBY_STRIPE_PUBLISHABLE_KEY=

Here are a couple commands to get you started in development:

# Run the development environment which builds use-shopping-cart in watch-mode
# and starts the CRA example with Netlify functions
yarn dev

# Runs tests in watch-mode
yarn test

# Runs the documentation page locally
yarn dev:docs

Warning

Please make all README edits to /use-shopping-cart/README.md. All edits to /README.md will be overwritten on commit by /use-shopping-cart/README.md. Consider /README.md read-only.

We created this hook with create-react-hook.

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Kevin Cunningham

⚠️ 💻

Ian Jones

⚠️

Christopher Brown

⚠️ 💻 📖

Nick DeJesus

💻 ⚠️

Shodipo Ayomide

📖

Anders Bech Mellson

💻

Thor 雷神

📖 💻 ⚠️

Ryan Warner

📖

Horacio Herrera

📖

Brian Douglas

📖

Brittney Postma

📖

Prince Wilson

📖

This project follows the all-contributors specification. Contributions of any kind welcome!