Skip to content

Commit

Permalink
Add Stripe Checkout
Browse files Browse the repository at this point in the history
  • Loading branch information
excid3 committed Feb 16, 2021
1 parent dd3881d commit a6d1327
Show file tree
Hide file tree
Showing 18 changed files with 584 additions and 103 deletions.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ gemspec
gem "byebug"
gem "appraisal", github: "excid3/appraisal", branch: "fix-bundle-env"

gem "braintree", ">= 2.92.0", "< 4.0"
gem "stripe", ">= 2.8"
gem "paddle_pay", "~> 0.0.1"

# Test against different databases
gem "sqlite3", "~> 1.4"
gem "mysql2"
Expand Down
21 changes: 21 additions & 0 deletions app/views/pay/stripe/_checkout_button.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<%= button_tag title,
id: "checkout-#{session.id}",
class: local_assigns[:class],
style: (local_assigns[:class] || local_assigns[:style]) ? local_assigns[:style] : 'background-color:#6772E5;color:#FFF;padding:8px 12px;border:0;border-radius:4px;font-size:1em'
%>
<%= tag.div id: "error-for-#{session.id}" %>

<script>
(() => {
const checkoutButton = document.getElementById("checkout-<%= session.id %>");
checkoutButton.addEventListener('click', function () {
Stripe("<%= Pay::Stripe.public_key %>").redirectToCheckout({
sessionId: '<%= session.id %>'
}).then(function (result) {
if (result.error) {
document.getElementById('error-message').innerText = result.error.message;
}
});
});
})()
</script>
36 changes: 36 additions & 0 deletions docs/stripe.md
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
# Using Pay with Stripe

Stripe has multiple integrations:

* Stripe Checkout - Hosted pages for payments

## Stripe Checkout

[Stripe Checkout](https://stripe.com/docs/payments/checkout) allows you to simply redirect to Stripe for handling payments. The main benefit is that it's super fast to setup payments in your application, they're SCA compatible, and they will get improved automatically by Stripe.

![stripe checkout example](https://i.imgur.com/nFsCBCK.gif)

### How to use Stripe Checkout with Pay

1. Create a checkout session

Choose the checkout button mode you need and pass any required arguments. Read the [Stripe Checkout Session API docs](https://stripe.com/docs/api/checkout/sessions/create) to see what options are available.

```ruby
# Make sure the user's payment processor is Stripe
current_user.processor = :stripe

# One-time payments
@checkout_session = current_user.payment_processor.checkout(mode: "payment", line_items: "price_1ILVZaKXBGcbgpbZQ26kgXWG")

# Subscriptions
@checkout_session = current_user.payment_processor.checkout(mode: "subscription", line_items: "default")

# Setup a new card for future use
@subscription = current_user.payment_processor.checkout(mode: "setup")
```

2. Render the button

```erb
<%= render partial: "pay/stripe/checkout_button", locals: { session: @checkout_session, title: "Checkout" } %>
```
59 changes: 59 additions & 0 deletions lib/pay/stripe/billable.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module Pay
module Stripe
class Billable
include Rails.application.routes.url_helpers

attr_reader :billable

delegate :processor_id,
Expand All @@ -10,6 +12,12 @@ class Billable
:card_token,
to: :billable

class << self
def default_url_options
Rails.application.config.action_mailer.default_url_options
end
end

def initialize(billable)
@billable = billable
end
Expand Down Expand Up @@ -173,6 +181,57 @@ def save_pay_charge(object)

charge
end

# https://stripe.com/docs/api/checkout/sessions/create
#
# checkout(mode: "payment")
# checkout(mode: "setup")
# checkout(mode: "subscription")
#
# checkout(line_items: "price_12345", quantity: 2)
# checkout(line_items [{ price: "price_123" }, { price: "price_456" }])
# checkout(line_items, "price_12345", allow_promotion_codes: true)
#
def checkout(**options)
args = {
payment_method_types: ['card'],
mode: 'payment',
# These placeholder URLs will be replaced in a following step.
success_url: root_url,
cancel_url: root_url,
}

# Line items are optional
if (line_items = options.delete(:line_items))
args[:line_items] = Array.wrap(line_items).map do |item|
if item.is_a? Hash
item
else
{ price: item, quantity: options.fetch(:quantity, 1) }
end
end
end

::Stripe::Checkout::Session.create(args.merge(options))
end

# https://stripe.com/docs/api/checkout/sessions/create
#
# checkout_charge(amount: 15_00, name: "T-shirt", quantity: 2)
#
def checkout_charge(amount:, name:, quantity: 1, **options)
checkout(
line_items: {
price_data: {
currency: options[:currency] || 'usd',
product_data: { name: name },
unit_amount: amount,
},
quantity: quantity
},
**options
)
end
end
end
end
32 changes: 17 additions & 15 deletions test/dummy/app/controllers/stripe/charges/imports_controller.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
class Stripe::Charges::ImportsController < ApplicationController
def new
end
module Stripe
class Charges::ImportsController < ApplicationController
def new
end

def create
object = find_stripe_object
charge = Pay::Stripe::Webhooks::ChargeSucceeded.new.create_charge(User.first, object)
redirect_to stripe_charge_path(charge)
end
def create
object = find_stripe_object
charge = Pay::Stripe::Webhooks::ChargeSucceeded.new.create_charge(User.first, object)
redirect_to stripe_charge_path(charge)
end

private
private

def find_stripe_object
case params[:id]
when /^ch_/
Stripe::Charge.retrieve(params[:id])
when /^pi_/
Stripe::PaymentIntent.retrieve(params[:id]).charges.first
def find_stripe_object
case params[:id]
when /^ch_/
Stripe::Charge.retrieve(params[:id])
when /^pi_/
Stripe::PaymentIntent.retrieve(params[:id]).charges.first
end
end
end
end
62 changes: 32 additions & 30 deletions test/dummy/app/controllers/stripe/charges_controller.rb
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
class Stripe::ChargesController < ApplicationController
before_action :set_charge, only: [:show, :refund]
module Stripe
class ChargesController < ApplicationController
before_action :set_charge, only: [:show, :refund]

def index
@charges = Pay::Charge.where(processor: :stripe).order(created_at: :desc)
end
def index
@charges = Pay::Charge.where(processor: :stripe).order(created_at: :desc)
end

def show
end
def show
end

def new
end
def new
end

def create
current_user.processor = params[:processor]
current_user.card_token = params[:card_token]
charge = current_user.charge(params[:amount])
redirect_to stripe_charge_path(charge)
rescue Pay::ActionRequired => e
redirect_to pay.payment_path(e.payment.id)
rescue Pay::Error => e
flash[:alert] = e.message
render :new
end
def create
current_user.processor = params[:processor]
current_user.card_token = params[:card_token]
charge = current_user.charge(params[:amount])
redirect_to stripe_charge_path(charge)
rescue Pay::ActionRequired => e
redirect_to pay.payment_path(e.payment.id)
rescue Pay::Error => e
flash[:alert] = e.message
render :new
end

def refund
@charge.refund!
rescue Pay::Error => e
flash[:alert] = e.message
ensure
redirect_to stripe_charge_path(@charge)
end
def refund
@charge.refund!
rescue Pay::Error => e
flash[:alert] = e.message
ensure
redirect_to stripe_charge_path(@charge)
end

private
private

def set_charge
@charge = Pay::Charge.find(params[:id])
def set_charge
@charge = Pay::Charge.find(params[:id])
end
end
end
10 changes: 10 additions & 0 deletions test/dummy/app/controllers/stripe/checkouts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Stripe
class CheckoutsController < ApplicationController
def show
current_user.processor = :stripe
@payment = current_user.payment_processor.checkout(mode: "payment", line_items: "price_1ILVZaKXBGcbgpbZQ26kgXWG")
@subscription = current_user.payment_processor.checkout(mode: "subscription", line_items: "default")
@setup = current_user.payment_processor.checkout(mode: "setup")
end
end
end
18 changes: 10 additions & 8 deletions test/dummy/app/controllers/stripe/payment_methods_controller.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
class Stripe::PaymentMethodsController < ApplicationController
def edit
@setup_intent = ::Stripe::SetupIntent.create
end
module Stripe
class PaymentMethodsController < ApplicationController
def edit
@setup_intent = ::Stripe::SetupIntent.create
end

def update
current_user.processor = params[:processor]
current_user.update_card(params[:card_token])
redirect_to payment_method_path
def update
current_user.processor = params[:processor]
current_user.update_card(params[:card_token])
redirect_to payment_method_path
end
end
end
102 changes: 52 additions & 50 deletions test/dummy/app/controllers/stripe/subscriptions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,52 +1,54 @@
class Stripe::SubscriptionsController < ApplicationController
before_action :set_subscription, only: [:show, :edit, :update, :destroy, :cancel, :resume]

def index
@subscriptions = Pay::Subscription.where(processor: :stripe).order(created_at: :desc)
end

def show
end

def new
end

def create
current_user.processor = params[:processor]
current_user.card_token = params[:card_token]
subscription = current_user.subscribe(plan: params[:price_id])
redirect_to stripe_subscription_path(subscription)
rescue Pay::ActionRequired => e
redirect_to pay.payment_path(e.payment.id)
rescue Pay::Error => e
flash[:alert] = e.message
render :new
end

def edit
end

def update
end

def destroy
@subscription.cancel_now!
redirect_to stripe_subscription_path(@subscription)
end

def cancel
@subscription.cancel
redirect_to stripe_subscription_path(@subscription)
end

def resume
@subscription.resume
redirect_to stripe_subscription_path(@subscription)
end

private

def set_subscription
@subscription = Pay::Subscription.where(processor: :stripe).find(params[:id])
module Stripe
class SubscriptionsController < ApplicationController
before_action :set_subscription, only: [:show, :edit, :update, :destroy, :cancel, :resume]

def index
@subscriptions = Pay::Subscription.where(processor: :stripe).order(created_at: :desc)
end

def show
end

def new
end

def create
current_user.processor = params[:processor]
current_user.card_token = params[:card_token]
subscription = current_user.subscribe(plan: params[:price_id])
redirect_to stripe_subscription_path(subscription)
rescue Pay::ActionRequired => e
redirect_to pay.payment_path(e.payment.id)
rescue Pay::Error => e
flash[:alert] = e.message
render :new
end

def edit
end

def update
end

def destroy
@subscription.cancel_now!
redirect_to stripe_subscription_path(@subscription)
end

def cancel
@subscription.cancel
redirect_to stripe_subscription_path(@subscription)
end

def resume
@subscription.resume
redirect_to stripe_subscription_path(@subscription)
end

private

def set_subscription
@subscription = Pay::Subscription.where(processor: :stripe).find(params[:id])
end
end
end
Loading

0 comments on commit a6d1327

Please sign in to comment.