Building a Shopping Cart with Redwood

Milecia

Having a shopping cart before you check out is something that good online stores have. It shows all of the items you want to be shipped to you and then you get to check out. It's important to have this functionality implemented securely so that you aren't putting users' information at risk

We're going to build a quick shopping cart with checkout abilities in Redwood, using Stripe to handle all of the checkout functionality.

Generating the project

There are a couple of things you'll want to do before we start writing code. First, create a Stripe account if you don't already have one. Create a Cloudinary account as well because this is where we'll pull the media for the items from. So go ahead and upload the images or videos you want to use for your items. We'll be using these two services throughout the app. The last thing you'll need is a local instance of Postgres. You can find the free install here.

Now we can go ahead and bootstrap the Redwood app by running the following line in a terminal. This will generate a TypeScript project for us.

1yarn create redwood-app --typescript shop-checkout

You'll notice a lot of new files and directories in the shop-checkout folder. The main folders we'll be working with are api, which holds the back-end code, and web, which holds the front-end code.

Since it's usually a good practice to start building the business logic first, let's start writing code for the back-end.

Working on the back-end

Inside api > db, you'll find a schema.prisma file. This is where all of your database changes happen. Whenever you make a change to a model in this file, you'll need to run a migration for that change to be reflected in your database.

The first thing we need to do is set up the connection to the database. Update the provider value from sqlite to postgresql. That tells Prisma that we're working with a Postgres database so it knows what to expect. Then we need to set the right value for DATABASE_URL.

To do this, look for the .env file in the root of the project. You'll see a line that's commented out with a DATABASE_URL value. Uncomment that line and update it to match your local connection string. That might look something like this where shop is the name we're giving the database.

1DATABASE_URL=postgres://admin:postgres@localhost:5432/shop

That's all we need to connect our back-end to the database. Now we need to define some models for the tables and rows we need data for.

Creating the models

You can delete the UserExample model and add the following models.

1model Item {
2 id String @id @default(uuid())
3 name String
4 price Float
5 url String
6 Order Order? @relation(fields: [orderId], references: [id])
7 orderId String?
8}
9
10model Order {
11 id String @id @default(cuid())
12 created_at DateTime
13 items Item[]
14 total Float
15}

We've created the models for the Item selections customers make in an Order. The main thing to note in these models is the relation between the two of these models. The orderId acts as a foreign key into the Order table. So there can be multiple items associated with one order.

Since we have the models, we can go ahead and run a migration with this command.

1yarn rw prisma migrate dev

You'll be prompted to name this migration after the connection has been established and you can call it anything you want.

Seeding the database

To keep the scope of this tutorial in focus, we're not going to build out the system for users to select items. Instead, we're going to seed the database with some items and orders to simulate having an order ready for check out.

In api > db, open the seed.js file. This is where we're going to write our seed data. You can delete all of the commented out code in the main function and add the code for our models.

1const ordersData = [
2 { createdAt: new Date('09/21/2021'), total: 123.34 },
3 { createdAt: new Date('04/21/2021'), total: 424.13 },
4 ]
5 const itemsData = [
6 { name: 'Spoon', price: 34.99, url: 'https://res.cloudinary.com/milecia/image/upload/v1606580786/samples/landscapes/landscape-panorama.jpg' },
7 { name: 'Blow Dryer', price: 89.99, url: 'https://res.cloudinary.com/milecia/image/upload/v1606580785/samples/landscapes/nature-mountains.jpg' },
8 { name: 'Pet Bed', price: 57.99, url: 'https://res.cloudinary.com/milecia/image/upload/v1606580779/samples/landscapes/architecture-signs.jpg' },
9 { name: 'Wicker Chair', price: 124.99, url: 'https://res.cloudinary.com/milecia/image/upload/v1606580776/samples/landscapes/girl-urban-view.jpg' },
10 { name: 'Paint', price: 42.99, url: 'https://res.cloudinary.com/milecia/video/upload/v1606580790/elephant_herd.mp4' },
11 { name: 'Flooring', price: 15.99, url: 'https://res.cloudinary.com/milecia/video/upload/v1606580788/sea-turtle.mp4' },
12 ]
13
14 return Promise.all(
15 ordersData.map(async (order) => {
16 const record = await db.order.create({
17 data: { createdAt: order.createdAt, total: order.total },
18 })
19 console.log(record)
20 }),
21 itemsData.map(async (item) => {
22 const record = await db.item.create({
23 data: {name: item.name, price: item.price, url: item.url, orderId: 'cktm7yt8l00001jxh5f26yg2z'},
24 })
25
26 console.log(record)
27 })
28 )

With this seed data in place, we can go ahead and seed the database with this command.

1yarn rw prisma db seed

This will add all of your data to the database, so when we get ready to load the shopping cart view there's data available for us.

Creating the GraphQL types and resolvers

The last thing we need to do on the back-end is add the methods we need to call the data on the front-end. We'll wrap this up pretty quick with a couple of Redwood commands to generate the GraphQL types and resolvers we need.

1yarn rw g sdl item
2yarn rw g sdl order

If you take a look in api > src > graphql, you'll see two new files. Take a look at the items.sdl.ts file and you'll see all fo the types you need to make your GraphQL queries and mutations.

Now take a look in api > src > services. There are two new folders here that hold a few different files. There are a couple of test files and the main file that holds the resolvers. Open either the items.ts file or the orders.ts file. You'll see the resolver to get all of the items or orders.

With that, we've finished the back-end! Now we can switch our attention to the front-end, where we'll build a quick shopping cart and check out page.

Taking it to the front-end

Before we start working on components, there is a package we'll need to add in order to work with Stripe. In a terminal, go to the web directory and run this command.

1yarn add react-stripe-checkout

This is the only package we needed to install, so we can get started on the shopping cart page.

Making the shopping cart

We'll use another Redwood command to generate the files we need for the shopping cart. In the terminal, go back to the root of the directory and run this command.

1yarn rw g page ShoppingCart /cart

This generates a page component that displays at the /cart route. If you take a look in web > src > page, you'll see a new folder named ShoppingCartPage. In this folder, there are couple of files related to tests and the main ShoppingCartPage.tsx file. Open this up and delete everything inside of the return statement of the component.

You can keep the <MetaTags> component if you like. I'm just deleting everything to keep things simple.

First, we'll add a GraphQL query to get the items. You can look up one of the order ids from the seeded data in your Postgres instance.

Remember, this is supposed to be about the shopping cart and checkout experience so we'll hard-code an order id. In a fully connected app, the order id would probably come from some parameter being passed from another component.

So let's add that GraphQL query above the ShoppingCartPage component.

1export const QUERY = gql`
2query Items {
3 items {
4 name
5 price
6 url
7 orderId
8 }
9}
10`

This will let us get all of the items we have seeded. Next, we need to execute this query so we have the data available and we're able to filter by order id. At the top of the file, add this import statement.

1import { useQuery } from '@redwoodjs/web'

Inside the ShoppingCartPage component, add this code at the top.

1const { data, loading } = useQuery(QUERY)
2
3if (loading) {
4 return <div>Loading...</div>
5}
6
7const orderItems = data.items.filter(item => item.orderId === 'cktm7yt8l00001jxh5f26yg2z')

This will fetch the items from the database and also return a loading state. When the data is loading, we need to handle that condition in the app so it doesn't crash. That's why we have that early return statement if the data is still loading.

Once it finishes loading, then we create a new variable to hold all of the items in the order. Now we need to display the info for each item. We'll add the following return statement to our component.

1return (
2 <div style={{
3 display: 'flex',
4 flexDirection: 'row',
5 flexWrap: 'wrap'
6 }}>
7 {orderItems.map(item => {
8 return (
9 <div style={{
10 border: '1px solid',
11 margin: '0 12px 24px',
12 padding: '24px',
13 height: '500px',
14 width: '20%'
15 }}>
16 {item.url.includes('mp4') ?
17 <video controls src={item.url} height={200} style={{ maxWidth: '100%' }}></video> :
18 <img src={item.url} height={200} style={{ maxWidth: '100%' }} />
19 }
20 <h2>{item.name}</h2>
21 <h3>{item.price}</h3>
22 </div>
23 )
24 })
25 }
26 </div>
27)

We have a couple of styled <div> elements to make the shopping cart look decent. For each of the items in the order, we check if its media is an image or a video and then we create a card to show the media, the name, and the price.

If you start your app with yarn rw dev in a terminal in the root directory and go the 'cart' route, you'll see something similar to this.

Now we need to add the Stripe elements to handle the check out after we click the button we're about to create.

Adding Stripe for checkouts

Let's start by importing that Stripe package. This is a good place to go grab the 'Publishable key' from your Stripe account because you'll need it for the component we're about to work with.

1import StripeCheckout from 'react-stripe-checkout';

This adds a simple checkout modal on the page when the customer is ready to check out. All of the checkout and payment functionality is bundled into this one component. We'll add the component above our list of items.

1<StripeCheckout
2 amount={36694}
3 billingAddress
4 description={`The ${orderItems.length} items you selected are here`}
5 image="https://res.cloudinary.com/milecia/image/upload/v1606580786/samples/landscapes/landscape-panorama.jpg"
6 locale="auto"
7 name="Your Cart"
8 stripeKey={process.env.STRIPE_ID}
9 token={processToken}
10 zipCode
11/>

This component has a ton of props and it would be worthwhile to check out the docs on them. We have the amount which could be calculated dynamically, but you'll have to watch out for the format expected by the component. Then we include the billingAddress to show this part of the process.

The main two props to note are the stripeKey and the token. The stripeKey is that publishable key you grabbed from your account earlier. You'll notice that token is a callback that will have the token from the successful Stripe transaction.

We'll define the method for processToken right above the return statement in our component.

1const processToken = (token) => {
2 console.log('This is where some back-end things would happen after a successful transaction.')
3}

Usually you'll pass that token to the back-end and do some processing before saving a record to your database. This is out of scope for this particular tutorial, but if you want to learn more about that back-end implementation, check out Stripe's docs on it.

Everything's in place and ready to go! If you take a look in your browser, you'll see a new button in the top left of the page.

Go ahead and click the button to see the checkout flow in action.

Since we're using a test account, you'll see a yellow indicator in the upper right corner of your page that says you're in test mode. Now if you continue, this will take you to where a customer can input their card info.

When you submit, you'll see a loading icon in the pay button and then a green checkmark confirming the transaction.

With a successful transaction, the token gets passed to the processToken function we made so you'll see our message in the console.

Now you have a shopping cart with Stripe checkout functionality!

Finished code

You can see the full code in the shop-checkout folder in this repo and you can see some of this in action in this Code Sandbox.

Conclusion

Knowing how to handle payments is a valuable developer skill because there are so many platforms and applications that rely on payment systems being implemented correctly and securely. It gives you the flexibility to make a site that does everything a customer needs.

Milecia

Software Team Lead

Milecia is a senior software engineer, international tech speaker, and mad scientist that works with hardware and software. She will try to make anything with JavaScript first.