Jumpstart docs logo Back to the app

One-time Payments

Selling products as one-time purchases is also easy with Jumpstart Pro. This is a multi-step process that starts with presenting the customer with checkout and then fulfilling the purchase once completed.

Checkout Form

The first step is to create a checkout form that handles one-time payments. We recommend adding metadata to the checkout for accessing later in fulfillment.

For Stripe, you'll create a Checkout Session and redirect or embed the form on the page (see subscriptions/forms/stripe). We'll use hosted Stripe Checkout for our example.

def new
  @checkout_session = payment_processor.checkout(
    mode: "payment",
    line_items: "price_1ILVZaKXBGcbgpbZQ26kgXWG",
    allow_promotion_codes: true,
    return_url: checkout_completed_url
  )
  redirect_to @checkout_session.url, allow_other_host: true
end

For Paddle Billing, checkout happens in JavaScript.

<%= tag.div data: {
  controller: "paddle--billing",
  paddle__billing_environment_value: Pay::PaddleBilling.environment,
  paddle__billing_client_token_value: Pay::PaddleBilling.client_token,
  paddle__billing_target: "form",
  paddle__billing_email_value: current_account.email,
  paddle__billing_items_value: [price_id: @price_id].to_json,
  paddle__billing_success_url_value: checkout_completed_url,
  paddle__billing_custom_data: {order: @order.id}.to_json
} %>

Checkout Completed Controller

Webhooks are the most reliable way of fulfilling a purchase but redirects after completed purchase also help ensure fulfillment happens as the user finishes in case webhooks are delayed.

In the previous step, we used a checkout_completed_url to redirect the user after checkout. You'll need to define a route for this and matching controller action to sync purchases.

class Checkout::CompletedController < ApplicationController
  def show
   # For Stripe
   @charge = Pay::Stripe::Charge.sync_from_checkout_session(params[:session_id])

   # For Paddle Billing
   current_account.set_payment_processor :paddle_billing, processor_id: params[:user_id]
   @transaction = Pay::PaddleBilling::Charge.sync_from_transaction(params[:transaction_id])
  end
end

The controller is only responsible for triggering the sync to the database. This is the exact same process as webhooks and ensures that any delays with webhooks don't affect the customer's checkout flow.

Fulfillment

Now that checkout is setup and redirects are handled, we can implement the fulfillment step.

# config/initializers/pay.rb
module PayChargeExtension
  extend ActiveSupport::Concern

  # Trigger fulfillment on create
  after_commit :fulfill_purchase, on: :create

  def fulfill_purchase
    # Look up record from metadata
    order = Order.find(metadata["course_id"])

    # Skip if already processed
    return if order.completed?

    # Handle fulfillment and mark it as complete
    order.update(status: :completed)
  end
end

Rails.configuration.to_prepare do
  Pay::Charge.include PayChargeExtension
end