Overview

The Spree checkout process has been designed for maximum flexibility. It’s been redesigned several times now, each iteration has benefited from the feedback of real-world deployment experience. It is relatively simple to customize the checkout process to suit your needs. Secure transmission of customer information is possible via SSL and credit card information is never stored in the database.

The Checkout Flow DSL

Spree comes with a new checkout DSL that allows you succinctly define the different steps of your checkout. This new DSL allows you to customize just the checkout flow, while maintaining the unrelated admin states, such as “canceled” and “resumed”, that an order can transition to. Ultimately, it provides a shorter syntax compared with overriding the entire state machine for the Spree::Order class.

The default checkout flow for Spree is defined like this, adequately demonstrating the abilities of this new system:

checkout_flow do
  go_to_state :address
  go_to_state :delivery
  go_to_state :payment, if: ->(order) {
    order.update_totals
    order.payment_required?
  }
  go_to_state :confirm, if: ->(order) { order.confirmation_required? }
  go_to_state :complete
  remove_transition from: :delivery, to: :confirm

we can pass a block on each checkout step definition and work some logic to figure if the step is required dynamically. e.g. the confirm step might only be necessary for payment gateways that support payment profiles.

These conditional states present a situation where an order could transition from delivery to one of payment, confirm or complete. In the default checkout, we never want to transition from delivery to confirm, and therefore have removed it using the remove_transition method of the Checkout DSL. The resulting transitions between states look like the image below:

These two helper methods are provided on Spree::Order instances for your convenience:

  • checkout_steps - returns a list of all the potential states of the checkout.
  • has_step? - Used to check if the current order fulfills the requirements for a specific state.

If you want a list of all the currently available states for the checkout, use the checkout_steps method, which will return the steps in an array.

Default Checkout Steps

The Spree checkout process consists of the following steps. With the exception of the Registration step, each of these steps corresponds to a state of the Order object:

  • Registration Optional - only if using spree_auth_devise extension, can be toggled through the Spree::Auth::Config[:registration_step] configuration setting
  • Address Information
  • Delivery Options Shipping Method
  • Payment
  • Confirmation

The following sections will provide a walk-through of checkout from a user’s perspective, and offer some information on how to configure the default behavior of the various steps.

Registration

This section only applies when using spree_frontend and spree_auth_devise gems.

Prior to beginning the checkout process, the customer will be prompted to create a new account or to log in to their existing account. By default, there is also a “guest checkout” option that allows users to specify only their email address if they do not wish to create an account.

Technically, the registration step is not an actual state in the Spree::Order state machine. The spree_auth_devise gem (an extension that comes with Spree Starter by default) adds the check_registration before filter to all actions of Spree::CheckoutController except for obvious reasons the registration and update_registration actions, which redirects to a registration page unless one of the following is true:

  • Spree::Auth::Config[:registration_step] preference is not true
  • user is already logged in
  • the current order has an email address associated with it

The method is defined like this:

def check_registration
  return unless Spree::Auth::Config[:registration_step]
  return if spree_current_user || current_order.email
  store_location
  redirect_to spree.checkout_registration_path
end

Disabling guest checkout

To completely disable guest checkout and require registration for checkout, you need to change Spree configuration in config/initializers/spree.rb:

Spree::Config[:allow_guest_checkout] = false

Address Information

This step allows the customer to add both their billing and shipping information. Customers can click the “use billing address” option to use the same address for both.

The address fields include a select box for choosing state/province. If there are no states configured for a particular country, the select box will be replaced by a text field instead.

The list of countries that appear in the country select box can also be configured. Spree will list all countries by default, but you can configure exactly which countries you would like to appear. The list can be limited to a specific set of countries by setting the Store’s checkout zone. Spree assumes that the list of billing and shipping countries will be the same. You can always change this logic via class decorator if this does not suit your needs.

Delivery Options

During this step, the user may choose a delivery method. Spree assumes the list of shipping methods to be dependent on the shipping address.

Payment

This step is where the customer provides payment information. This step is intentionally placed last in order to minimize security issues with credit card information. Credit card information is never stored in the database so it would be impossible to have a subsequent step and still be able to submit the information to the payment gateway. Spree submits the information to the gateway before saving the model so that the sensitive information can be discarded before saving the checkout information.

Spree stores only the last four digits of the credit card number along with the expiration information. The full credit card number and verification code are never stored in the Spree database.

Several gateways such as ActiveMerchant and Beanstream provide a secure method for storing a “payment profile” in your database. This approach typically involves the use of a “token” which can be used for subsequent purchases but only with your merchant account. If you are using a secure payment profile it would then be possible to show a final “confirmation” step after payment information is entered.

If you do not want to use a gateway with payment profiles then you will need to customize the checkout process so that your final step submits the credit card information. You can then perform authorization before the order is saved. This is perfectly secure because the credit card information is not ever saved. It’s transmitted to the gateway and then discarded like normal.

Spree discards the credit card number after this step is processed. If you do not have a gateway with payment profiles enabled then your card information will be lost before it’s time to authorize the card.

For more information about payments, please see the Payments guide.

Confirmation

This is the final opportunity for the customer to review their order before submitting it to be processed. Users have the opportunity to return to any step in the process using either the back button or by clicking on the appropriate step in the “progress breadcrumb.”

This step is disabled by default, but can be enabled by two ways:

  1. globally for all orders by setting a preference in config/initializers/spree.rb:

    Spree::Config[:always_include_confirm_step] = true
    
  2. conditionally for specific orders by overriding the confirmation_required? method in Spree::Order, eg.

    module Spree
      module OrderDecorator
        # require confirmation for orders with a US billing address
        def confirmation_required?
          billing_address&.country_iso == 'US'
        end
      end
    end
    

Adding Logic Before or After a Particular Step

The state_machines gem allows you to implement callbacks before or after transitioning to a particular step. These callbacks work similarly to Active Record Callbacks in that you can specify a method or block of code to be executed prior to or after a transition. If the method executed in a before_transition returns false, then the transition will not execute.

So, for example, if you wanted to verify that the user provides a valid zip code before transitioning to the delivery step, you would first implement a valid_zip_code? method, and then tell the state machine to run this method before that transition, placing this code in a file called app/models/spree/order_decorator.rb:

module Spree
  module OrderDecorator
    def self.prepended(base)
      base.state_machine.before_transition to: :delivery, do: :valid_zip_code?
    end
    
    def valid_zip_code?
      # logic to check if the zip code is valid
    end
  end

  Order.prepend(OrderDecorator)
end

This callback would prevent transitioning to the delivery step if valid_zip_code? returns false.

Modifying the checkout flow

To add or remove steps to the checkout flow, you can use the insert_checkout_step and remove_checkout_step helpers respectively.

The insert_checkout_step method takes a before or after option to determine where to insert the step:

module Spree
  module OrderDecorator
    def self.prepended(base)
      base.insert_checkout_step :new_step, before: :address
      # or
      # base.insert_checkout_step :new_step, after: :address
    end
  end

  Order.prepend(OrderDecorator)
end

The remove_checkout_step will remove just one checkout step at a time:

module Spree
  module OrderDecorator
    def self.prepended(base)
      base.remove_checkout_step :address
      base.remove_checkout_step :delivery
    end
  end

  Order.prepend(OrderDecorator)
end

What will happen here is that when a user goes to checkout, they will be asked to potentially fill in their payment details and then potentially confirm the order. This is the default behavior of the payment and the confirm steps within the checkout. If they are not required to provide payment or confirmation for this order then checking out this order will result in its immediate completion.

To completely re-define the flow of the checkout, use the checkout_flow helper:

module Spree
  module OrderDecorator
    def self.prepended(base)
      base.checkout_flow do
        go_to_state :payment
        go_to_state :complete
      end
    end
  end

  Order.prepend(OrderDecorator)
end