How to build an online store using React and Rails - Part 2

Tutorial code - https://github.com/learnetto/stickers.win

In Part 1, we created React components for displaying multiple products, a shopping cart and buttons for adding products and removing them from the cart.

Now that we have all the basic visual components in place, let’s make them functional so that we can add items to the cart and checkout.

First, we need to add click handlers to the buttons so that we can update the cart state with the right items and calculate the total amount due.

Let’s add a click handler to the Add button:

# app/javascript/packs/Store/Product.js

<button onClick={this.handleAdd}>+ Add to cart</button>
And to the Remove button:

# app/javascript/packs/Store/Product.js

<button onClick={this.handleRemove}>- Remove from cart</button>
Then let’s define these functions. First handleAdd:

# app/javascript/packs/Store/Product.js

  handleAdd = () => {
    this.props.handleAdd(this.props.product.id)
  }
We’re just going to call another function also called handleAdd that this component will receive as a prop. And we’ll pass the product id to it.

Let’s define handleRemove similarly:

# app/javascript/packs/Store/Product.js

  handleRemove = () => {
    this.props.handleRemove(this.props.product.id)
  }

We’re going to define the actual functions which will update state in the Store component, and simply pass them down as props first to ProductsList and then Product.

One issue we have with this code is that we actually need to send Stripe the product SKU id, not the product id. It can be confusing, but remember those two are different. The SKU is the specific variation which we are selling. So let’s change these functions to pass the SKU id instead.

# app/javascript/packs/Store/Product.js

  handleAdd = () => {
    this.props.handleAdd(this.props.product.skus.data[0].id)
  }

  handleRemove = () => {
    this.props.handleRemove(this.props.product.skus.data[0].id)
  }
Now it would be better if we sent down the Stripe data to the UI in the exact format that we need. We should also move the price formatting code into a separate utility function. But we can do all that as a refactor later on.

Let’s get something working for now!

Now the Product component is rendered inside the ProducstList component, so let’s pass the handler functions as props from there:

# app/javascript/packs/Store/ProductsList.js

const ProductsList = ({products, handleAdd, handleRemove}) =>
  <div>
    {products.map(product => {
      return(<Product key={product.id} product={product}
              handleAdd={handleAdd} handleRemove={handleRemove} />)
    })}
  </div>
And finally, let’s pass them to ProductsList from the render method of the Store component:

# app/javascript/packs/Store/index.js

  render () {
    return (
      <div>
        <Cart cart={this.state.cart} />
        <ProductsList products={this.state.products}
          handleAdd={this.addToCart} handleRemove={this.removeFromCart} />
      </div>
    )
  }
We’ll call them addToCart and removeFromCart.

Now, let’s define them. They’ll accept sku as an argument:

# app/javascript/packs/Store/index.js

  addToCart = (sku) => {

  }

  removeFromCart = (sku) => {

  }
Let’s define addToCart first:

# app/javascript/packs/Store/index.js

  addToCart = (sku) => {
    let cart = this.state.cart
    cart.items[sku] = cart.items[sku] + 1 || 1
    this.setState( { cart })
  }

We’ll make a copy of the cart in state. Then we’ll increase the count of this product’s sku by 1. If the sku key doesn’t exist in the cart, we’ll just set it to 1.

We need set the initial state value of cart items in the constructor to an empty object:


# app/javascript/packs/Store/index.js

class Store extends Component {
  constructor(props) {
    super(props)
    this.state = {
      products: this.props.products,
      cart: {
        items: {},
        total: 0
      }
    }
  }

Now we can test this in the browser by checking the value of state.cart.items in the React developer tools tab. It should change according to the items and their quantities you add to the cart.

Ok, so now we can add the items to the cart and increment their count. But we’re not updating the cart total amount displayed to the user yet.
So let’s do that next.

Remember, we’re going to calculate the cart total on the client side only for the purpose of displaying the value to the user. We don’t trust this value to make actual charges.

In most live ecommerce stores, the cart amount is always calculated on the server, even just for displaying to the user.

We can do that too but it will mean making calls to our server and the Stripe API every time we add or remove an item from the cart. It’s too much work for this tutorial, so let’s keep it simple and calculate the amount on the client.

To get price, let’s pass the full sku data to the click handler methods:

# app/javascript/packs/Store/Product.js

  handleAdd = () => {
    this.props.handleAdd(this.props.product.skus.data[0])
  }

  handleRemove = () => {
    this.props.handleRemove(this.props.product.skus.data[0])
  }
Then we can get both id and price from the sku and add the item's price to the cart total.

# app/javascript/packs/Store/index.js

  addToCart = (sku) => {
    let cart = this.state.cart
    cart.items[sku.id] = cart.items[sku.id] + 1 || 1
    cart.total += sku.price
    this.setState( { cart })
  }

Now let’s define removeFromCart in a similar way.

Instead of increasing the item count, we need to decrease it by 1 and we’ll subtract the item price from the total.
But we have to take care of one extra concern in this method. We have to make sure we only have positive values.
So we must check if the item exists in the cart and if its count is greater than zero.

# app/javascript/packs/Store/index.js

  removeFromCart = (sku) => {
    let cart = this.state.cart
    if(sku.id in cart.items && cart.items[sku.id] > 0) {
      cart.items[sku.id] = cart.items[sku.id] - 1
      cart.total -= sku.price
      this.setState( { cart })
    }
  }
Now we can add and remove items to the cart and display a total amount. Now let’s display a checkout button.

We’re going to use a React component library called react-stripe-checkout for adding the Stripe checkout button.

This library provides the Stripe checkout button as a React component. We can also use Stripe checkout without it but then we need to make sure that the checkout Javascript loads before we render the button. Otherwise we run into a common issue where the Stripe checkout button doesn’t load. The react-stripe-checkout component takes care of that for us.

So let’s first install it:

$ yarn add react-stripe-checkout
Then we’re going to use it inside the Cart component. So let’s first import it:

# app/javascript/packs/Store/Cart.js

import StripeCheckout from 'react-stripe-checkout'

Then we want to display the checkout button only when we have at least 1 item in the cart, i.e. the cart total is more than 0 dollars.

# app/javascript/packs/Store/Cart.js

      Total: ${cart.total/100}
      {cart.total > 0 &&
        <StripeCheckout />
      }

Now when you test the app in the browser, as soon as you add an item to the cart, the stripe button should appear. And when you remove the item, it should disappear again.

Let’s add a little bit of left margin to the checkout button.

# app/assets/stylesheets/store.scss

.cart {
  float: right;
  padding: 10px;
  background: rgba(255, 161, 161, 0.21);
  text-align: right;
  margin-top: 4px;
  clear:both;

  button.StripeCheckout {
    margin-left: 20px;
  }
}
Changing the button colour and other styles is a little bit more complicated because we need to use custom elements. So we’ll leave that for now.

Now we need to pass some props to the StripeCheckout component so that it renders the Stripe checkout form with all the information it needs to accept a payment from the user - the stripe key, description of the product being purchased, amount to be charged, a label for the payment button, and a boolean value true for asking for the user’s billing and shipping address.

# app/javascript/packs/Store/Cart.js

<StripeCheckout
  token={onToken}
  stripeKey="pk_test_71JVZVV9uaQiRATI7nMKen5E"
  description="Stickers from stickers.win"
  amount={cart.total}
  label="Checkout"
  billingAddress={true}
  shippingAddress={true} />

These props correspond to the data attributes we’d use in a plain script tag for adding a Stripe checkout button. 
The token prop’s value is set to a function onToken which will use the payment token returned by Stripe to make further calls to the Stripe API. We’ll pass onToken as a prop to Cart from the Store component.

So let’s add onToken to the list of props Cart receives:

# app/javascript/packs/Store/Cart.js

const Cart = ({cart, onToken}) => (

And then let’s pass it from Store:

# app/javascript/packs/Store/index.js

<Cart cart={this.state.cart} onToken={this.onToken} />
Now let’s define the onToken function. We’re going to take the token and make a POST request to our charges controller. Let’s use axios for making the Ajax call.

First let’s install it:

$ yarn add axios

And import it in Store/index.js:

# app/javascript/packs/Store/index.js

import axios from 'axios'
Now we need to post the token and the cart data to the charges controller.
First we’ll define the data object with token and the cart from state:

# app/javascript/packs/Store/index.js

  onToken = (token) => {
    const data = {...token, cart: this.state.cart }

And then we’ll use axios to POST the data. We need to include the CSRF token in a header.

# app/javascript/packs/Store/index.js

    axios({
      url: '/charges',
      method: 'POST',
      data: data,
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
      }
    })
    .then(response => {
      
    })
  }
That will return a promise so we’ll then handle the response.

Now let’s open charges controller to handle this posted data.

Here we have some code for making a charge for a single product with a hard coded amount. We used this in a previous tutorial where we built a simple store with just one product.

# app/controllers/charges_controller.rb

    # Amount in cents
    @amount = 500

    customer = Stripe::Customer.create(
      :email => params[:stripeEmail],
      :source  => params[:stripeToken]
    )

    charge = Stripe::Charge.create(
      :customer    => customer.id,
      :amount      => @amount,
      :description => 'pack of 5 stickers',
      :currency    => 'usd'
    )

So let’s remove this code now. We’re going to replace it with code that will use the Stripe Orders API.

We’ll use the data posted from the client to first create an order and then charge the customer for it.

# app/javascript/packs/Store/index.js

  def create
    items = []
    params[:cart][:items].each do |k, v|
      items << { :type => 'sku', :parent => k, :quantity => v }
    end
    order = Stripe::Order.create(
      :email => params[:email],
      :currency => 'usd',
      :items => items,
      :shipping => {
        :name => params[:card][:name],
        :address => {
          :city => params[:card][:address_city],
          :country => params[:card][:address_country],
          :line1  => params[:card][:address_line1],
          :line2 => params[:card][:address_line2],
          :state => params[:card][:address_state],
          :postal_code => params[:card][:address_zip]
        }
      }
    )

    token = params[:id]
    order.pay(:source => token)

    render json: {message: "Thanks for your purchase. Your stickers are on the way!"}
  rescue Stripe::CardError => e
    flash[:error] = e.message
    redirect_to :root
  end

We pass a number of required and optional arguments to Stripe::Order.create.
Stripe requires this data in a specific format. So we’ll loop through our cart items and format each item.
we could use map but Rails doesn’t allow using map on an unpermitted param. So we have to use this workaround with each.

Shipping itself is an optional argument, but if you do use it, then name and the first line of address are required. the rest are optional.

After creating the Stripe order, we have to write a separate call to charge the customer.

We’ll first get the token from the params and then call order.pay with the source argument set to token. That will charge the customer for this order.

Notice that we are not specifying an amount anywhere.

Stripe will automatically calculate the total amount based on the items in our order because it already has all the price information.

Once the customer has successfully paid for the order, we want to show them a success message. So we render that as a json in the response of this action.

Now let’s use this response in the Store component.

We’ll get the message from response.data.message and then we’ll display that through a value in state.

# app/javascript/packs/Store/index.js

    .then(response => {
      let message = response.data.message
      this.setState({message: message})
    })

We need to initialise message as a blank string in the constructor:

# app/javascript/packs/Store/index.js

    this.state = {
      products: this.props.products,
      cart: {
        items: {},
        total: 0
      },
      message: ''
    }
Then let’s display that in the render method:

# app/javascript/packs/Store/index.js

<h3>{this.state.message}</h3>

The other thing we want to do on a successful purchase is empty the cart.

So let’s also reset the cart to its initial empty value in this setstate call.

# app/javascript/packs/Store/index.js

this.setState({message: message, cart: { items: {}, total: 0}})

Now you can test the app in the browser and you should be able to add and remove cart items, fill in the checkout form, submit it and see the success message.

You can also check your Stripe dashboard to make sure the order appears there with the customer email, the items, shipping address and a confirmation of successful payment.

We have a lot of Stripe API code in our charges controller. Ideally we should refactor this into a service object.
For example we could define a StripeService, which handles all the Stripe stuff - parsing and formatting the params and making API calls to Stripe.
We could also use it for formatting our store product data so that we don’t have to parse deeply nested sku data on the client.

So that’s our Rails ecommerce store working with the Stripe Orders API, Checkout js and React components.

Liked this tutorial? Get more like this in your inbox