Skip to main content
Version: 4.0

Redefining checkout steps

Solidus comes bundled with a robust checkout system that caters to the needs of the majority of eCommerce stores. However, this system doesn't fit the needs for every business.

In this guide, we'll cover the steps you need to take to customize the checkout flow in your Solidus store.

The default checkout flow

The default Solidus checkout flow follows these steps, from start to finish:

  1. Cart
  2. Address
  3. Delivery
  4. Payment (if needed)
  5. Confirm
  6. Complete

Removing checkout steps

You'll need to override the order model to add or remove checkout steps. Inside the override, you just need to add remove_checkout_step and pass in the name of the step you want to remove. For example, if you wanted to remove the address step, your override might look like this:

app/overrides/my_app/spree/order/remove_checkout_step.rb
# frozen_string_literal: true

module MyApp
module Spree
module Order
module RemoveCheckoutStep
def self.prepended(base)
base.remove_checkout_step :address
end

::Spree::Order.prepend self
end
end
end
end

Please keep in mind the following caveats for removing checkout steps:

  • If you remove the address step but keep the delivery step, the delivery step will break, as it expects the order to have an address.
  • If you remove the payment step but the order has a positive balance, the order will not be able to complete.

Adding checkout steps

If you want to add a custom checkout step, you will need to call add_checkout_step in the order override, and pass in your custom step name, as well as a before or after attribute, so that the order state machine knows where to put this custom step in the checkout flow.

app/overrides/my_app/spree/order/add_checkout_step.rb
# frozen_string_literal: true

module MyApp
module Spree
module Order
module AddCheckoutStep
def self.prepended(base)
base.insert_checkout_step :my_custom_step, { before: :confirm }
end

::Spree::Order.prepend self
end
end
end
end

You will also need to add a view for this custom step:

app/views/spree/checkout/_my_custom_step.html.erb
<div>
The customer will see this partial when they are on your checkout step.
Be sure to add a continue button!
</div>

<div class="form-buttons" data-hook="buttons">
<%= submit_tag t('spree.save_and_continue'), class: 'continue button primary' %>
<script>Spree.disableSaveOnClick();</script>
</div>

And finally, our step needs a translation that Solidus will display to the user:

config/locales/en.yml
en:
spree:
order_state:
my_custom_step: My Custom Step

Before step action

Before proceeding to any step, the checkout controller will check to see if a method named before_#{checkout_step_name} exists. If it does, it runs that method. That means that you can run custom logic before the controller moves on to your step, which can be handy if you want to set things up for the view.

app/overrides/my_app/spree/checkout_controller/add_before_custom_step.rb
# frozen_string_literal: true

module MyApp
module Spree
module CheckoutController
module AddBeforeCustomStep
def before_my_custom_step
@custom_object = CustomObject.new()
end

::Spree::CheckoutController.prepend self
end
end
end
end

Checkout Controller Attributes

If you need to permit additional attributes for your custom step, you can add the attributes to the checkout_confirm_attributes set in an initializer.

config/initializers/my_app_initializer.rb
Spree::PermittedAttributes.checkout_my_custom_step_attributes << [:my_custom_attribute]
danger

The attribute set that is used changes depending on the step: payment uses checkout_payment_attributes, address uses checkout_address_attributes, and so on. checkout_confirm_attributes is used for the confirm step, and for any step that the checkout controller does not recognize - like our new custom step.

Conditional checkout steps

You can conditionally include steps in the checkout flow if necessary, by passing a conditional in with the step in add_checkout_step. For example, if we only want to include our custom step if the orders total is over $50, we can pass that requirement in as a conditional:

app/overrides/my_app/spree/order/add_checkout_step.rb
# frozen_string_literal: true

module MyApp
module Spree
module Order
module AddCheckoutStep
def self.prepended(base)
base.insert_checkout_step :my_custom_step, {
before: :confirm, if: -> (order) { order.total > 50 }
}
end

::Spree::Order.prepend self
end
end
end
end

Now the custom step will only be displayed during checkout if the order total is above $50.00.

Overriding the state machine

If the customization options above are still not enough, you can override the entire state machine and use your own. We recommend that your new state machine inherit from the old one, so that you can utilize some of the logic of the old state machine.

First of all, you will have to define your own state machine:

lib/my_app/state_machine.rb
# frozen_string_literal: true

require 'spree/core/state_machines/order'

module MyApp
class StateMachine
module Order
include ::Spree::Core::StateMachines::Order

module ClassMethods
include ::Spree::Core::StateMachines::Order::ClassMethods

def define_state_machine!
# Go crazy!
end
end

def self.included(klass)
klass.extend ClassMethods
end
end
end
end

Once it's defined, you just need to tell Solidus to use it:

config/initializers/spree.rb
Spree.config do |config|
config.state_machines.order = "MyApp::StateMachine::Order"

# ....
end