8000 Slight refactoring by shey · Pull Request #14 · shey/rails-pay-checkout-demo · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Slight refactoring #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 1 addition & 43 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,31 @@ source "https://rubygems.org"

ruby "3.3.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.1.3", ">= 7.1.3.2"

# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"

# Use sqlite3 as the database for Active Record
gem "sqlite3", "~> 1.4"

# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"

# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"

# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"

# Use Tailwind CSS [https://github.com/rails/tailwindcss-rails]
gem "tailwindcss-rails"

# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Redis adapter to run Action Cable in production
gem "redis", ">= 4.0.1"

# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[windows jruby]

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[mri windows]
end

group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console"

# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"

# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
gem "standard"
end

group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"
end
Expand All @@ -75,9 +37,5 @@ end
gem "pay", "~> 7.1.1"
gem "receipts", "~> 2.0"
gem "stripe", "~> 10.12"

gem "standardrb", "~> 1.0"

gem "pg", "~> 1.5"

gem "faker", "~> 3.3"
4 changes: 1 addition & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,6 @@ GEM
standard-performance (1.3.1)
lint_roller (~> 1.1)
rubocop-performance (~> 1.20.2)
standardrb (1.0.1)
standard
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.0)
Expand Down Expand Up @@ -345,7 +343,7 @@ DEPENDENCIES
selenium-webdriver
sprockets-rails
sqlite3 (~> 1.4)
standardrb (~> 1.0)
standard
stimulus-rails
stripe (~> 10.12)
tailwindcss-rails
Expand Down
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Stripe integration with Rails and the Pay gem for Subscription Billing.
![Landing Page](docs/demo.png)

## Table of Contents

- [Running The App](#running-the-app)
- [Stripe Checkout](#stripe-checkout)
- [The sequence of events for Stripe Checkout are](#the-sequence-of-events-for-stripe-checkout-are)
- [Products and Prices](#products-and-prices)
Expand All @@ -22,40 +24,61 @@ Stripe integration with Rails and the Pay gem for Subscription Billing.
- [Relevant Files](#relevant-files)
- [Related Articles](#related-articles)

## Running the App

```bash
bin/setup # Installs gems, creates the database, and runs seeds
bin/dev # Starts the Rails server with foreman (Procfile.dev)
```

Note: You’ll need the Stripe CLI running for webhooks to be received

```bash
$ stripe listen --forward-to localhost:3000/pay/webhooks/stripe
```

## Stripe Checkout

**Stripe Checkout** Stripe Checkout is a checkout flow where Stripe hosts the payments page that collects the credit card details.

### The sequence of events for Stripe Checkout are:

1. A checkout session is created.
1. The user is redirected to Stripe's payment page.
1. The user completed a payment on Stripe's page.
1. The user is redirected back to the app.
1. The app receives a "payment success" webhook from Stripe.

### Products and Prices
[Products](https://dashboard.stripe.com/products) and prices are used for subscription billing in lieu of plans. In this app, each "plan" is its own product, and each product has a single [price](app/controllers/checkouts_controller.rb).

[Products](https://dashboard.stripe.com/products) and prices are used manage subscription billing. In this app, each plan is its own product, and each product has a single [price](app/controllers/checkouts_controller.rb).

![Stripe Product Catalogue Page](docs/product-catalogue.png)

## The Pay Integration

To complete the integration

1. Set up your payment processor credentials and [initializers](#initializers).
1. Add the `pay_customer` the [User](#pay_customer) Model.
1. Request a Checkout URL and [Redirect](#redirect) the User
1. Handle Stripe [Events](#events)

### Set up your payment processor credentials and initializers {#initializers}
### Set up your payment processor credentials and initializers {#initializers}

#### Stripe Credentials

Follow Pay's [configuration instructions](https://github.com/pay-rails/pay/blob/main/docs/2_configuration.md#configuring-pay) to set up your Stripe credentials.

#### Initializers

1. Create or update the [stripe.rb](config/initializers/stripe.rb) initializer.
1. Create the [pay.rb](config/initializers/pay.rb) initializer.

### Add the `pay_customer` Class Method to the User Model

1. **Generate the Pay Models**:

- Pay is already installed. For a fresh app, run `bin/rails pay:install:migrations` to create the necessary Pay models.

2. **Update the User Model**:
Expand All @@ -70,11 +93,12 @@ Including `pay_customer` in the User model establishes an internal association b

`payment_processor` is the entry point to Pay's functionality. By including pay_customer in the User model, the payment_processor method becomes available on all User instances, providing access to customer, subscription, and charge models.


### Request a Checkout URL and Redirect the User

Using Pay, request a checkout URL from Stripe and then redirect the user to that URL. Once the transaction is complete, Stripe will return the user to the URL defined by `success_URL`, along with a `session_id`.

#### Checkout URL Generator

```ruby
def checkout
user.payment_processor.checkout(
Expand All @@ -88,7 +112,9 @@ end
```

### Handle Stripe Events

#### Stripe CLI

The Pay Gem requires Stripe Webhooks to function properly. When building the Pay integration locally, you'll need to set up the [Stripe CLI](https://docs.stripe.com/stripe-cli) and have it running to forward the events to your app.

```bash
Expand All @@ -100,6 +126,7 @@ Update your stripe credentials to include the webhook signing secreted generated
![stripe events](docs/events.png)

#### The Payment Succeed Event

When Stripe processes a payment successfully, it triggers the invoice.payment_succeeded [event] (https://github.com/pay-rails/pay/tree/main/test/pay/stripe/webhooks). Use this event to initiate other workflows in Rails. For instance, access a paid feature or a custom receipt.

```ruby
Expand All @@ -124,6 +151,7 @@ end
## Appendix

### Relevant Files

1. [user.rb](app/models/user.rb)
1. [app/controllers/static_controller.rb](app/controllers/static_controller.rb)
1. [app/controllers/checkouts_controller.rb](app/controllers/checkouts_controller.rb?#L23)
Expand All @@ -134,6 +162,7 @@ end
1. [config/initializers/stripe.rb](config/initializers/stripe.rb)

### Related Articles

1. [Stripe Checkout](https://github.com/pay-rails/pay/blob/main/docs/stripe/8_stripe_checkout.md)
1. [Routes and Webhooks](https://github.com/pay-rails/pay/blob/main/docs/7_webhooks.md)
1. [Stripe Webhooks](https://github.com/pay-rails/pay/blob/main/docs/stripe/5_webhooks.md)
13 changes: 8 additions & 5 deletions app/controllers/checkouts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ class CheckoutsController < ApplicationController
before_action :set_price_id, only: %i[show]

def show
redirect_to stripe_checkout_url, allow_other_host: true, status: :see_other
redirect_to checkout_url, allow_other_host: true, status: :see_other
end

private

def stripe_checkout_url
def checkout_url
StripeCheckout.new(user, @stripe_price_id).url
end

Expand All @@ -16,11 +16,14 @@ def user
end

def set_price_id
plan_name = params[:plan]
@stripe_price_id = plan_price_id_mapping[plan_name.to_sym]
@stripe_price_id = price_id_map[plan_name]
end

def plan_price_id_mapping
def plan_name
params[:plan].to_sym
end

def price_id_map
{
enterprise: "price_1Ow66SAWnWpxHPD5qEZoUi4O",
professional: "price_1Ow66SAWnWpxHPD5K270wTji",
Expand Down
9 changes: 5 additions & 4 deletions app/services/stripe_checkout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ class StripeCheckout
include Rails.application.routes.url_helpers

attr_reader :user, :stripe_price_id

def initialize(user, stripe_price_id)
@user = user
@stripe_price_id = stripe_price_id
end

def success_url
root_url
end

def url
checkout.url
end

private

def success_url
root_url
end

def checkout
user.payment_processor.checkout(
mode: "subscription",
Expand Down
29 changes: 20 additions & 9 deletions app/views/static/home.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@
<h2 class="text-3xl font-bold text-pink-600">Pick Your Plan</h2>
</div>
<div class="flex flex-wrap gap-8 justify-center">
<!-- Card 1 -->
<!-- Hobby -->
<div class="flex-1 min-w-[250px] max-w-xs border border-gray-200 rounded-lg shadow-lg p-6 bg-gradient-to-r from-blue-100 to-blue-200 text-center transform hover:scale-105 transition-transform duration-300">
<h3 class="text-xl font-semibold mb-2 text-gray-800">Hobby</h3>
<p class="text-gray-600 mb-4">Perfect for personal projects and small-scale apps.</p>
<%= link_to 'Get Started', checkouts_path(plan: "hobby"), class: "bg-blue-500 hover:bg-pink-500 text-white py-2 px-6 rounded-full transition-colors duration-300", data: { turbo: false } %>
<%= link_to 'Get Started', checkouts_path(plan: 'hobby'), class: 'bg-blue-500 hover:bg-pink-500 text-white py-2 px-6 rounded-full transition-colors duration-300', data: { turbo: false } %>
</div>
<!-- Card 2 -->
<!-- Professional -->
<div class="flex-1 min-w-[250px] max-w-xs border border-gray-200 rounded-lg shadow-lg p-6 bg-gradient-to-r from-green-100 to-green-200 text-center transform hover:scale-105 transition-transform duration-300">
<h3 class="text-xl font-semibold mb-2 text-gray-800">Professional</h3>
<p class="text-gray-600 mb-4">Ideal for freelancers and growing businesses.</p>
<%= link_to 'Get Started', checkouts_path(plan: "professional"), class: "bg-green-500 hover:bg-pink-500 text-white py-2 px-6 rounded-full transition-colors duration-300", data: { turbo: false } %>
<%= link_to 'Get Started', checkouts_path(plan: 'professional'), class: 'bg-green-500 hover:bg-pink-500 text-white py-2 px-6 rounded-full transition-colors duration-300', data: { turbo: false } %>
</div>
<!-- Card 3 -->
<!-- Enterprise -->
<div class="flex-1 min-w-[250px] max-w-xs border border-gray-200 rounded-lg shadow-lg p-6 bg-gradient-to-r from-purple-100 to-purple-200 text-center transform hover:scale-105 transition-transform duration-300">
<h3 class="text-xl font-semibold mb-2 text-gray-800">Enterprise</h3>
<p class="text-gray-600 mb-4">Best for large organizations with advanced needs.</p>
<%= link_to 'Get Started', checkouts_path(plan: "enterprise"), class: "bg-purple-500 hover:bg-pink-500 text-white py-2 px-6 rounded-full transition-colors duration-300", data: { turbo: false } %>
<%= link_to 'Get Started', checkouts_path(plan: 'enterprise'), class: 'bg-purple-500 hover:bg-pink-500 text-white py-2 px-6 rounded-full transition-colors duration-300', data: { turbo: false } %>
</div>
</div>
</section>
Expand All @@ -33,10 +33,21 @@
<div class="flex-1 min-w-[300px] max-w-md border border-gray-200 rounded-lg shadow-lg p-6 bg-white text-center">
<h3 class="text-xl font-semibold mb-2 text-gray-800">User</h3>
<% if @subscription.present? %>
<p class="text-gray-600 mb-4">You are currently subscribed to the <span class="font-semibold text-pink-600"><%= @subscription.name %></span> plan.</p>
<p class="text-gray-600 mb-4">
You are currently subscribed to the
<span class="font-semibold text-pink-600"><%= @subscription.name %></span> plan.
</p>
<div class="text-left mb-4">
<p class="text-gray-600"><span class="font-semibold">Renewal Date:</span> <%= @subscription.current_period_end.strftime("%B %d, %Y") %></p>
<p class="text-gray-600"><span class="font-semibold">Price:</span> <span class="text-pink-600"><%= number_to_currency(@price.unit_amount / 100.0, unit: "$", precision: 2) %>/month</span></p>
<p class="text-gray-600">
<span class="font-semibold">Renewal Date:</span>
<%= @subscription.current_period_end.strftime('%B %d, %Y') %>
</p>
<p class="text-gray-600">
<span class="font-semibold">Price:</span>
<span class="text-pink-600">
<%= number_to_currency(@price.unit_amount / 100.0, unit: '$', precision: 2) %>/month
</span>
</p>
</div>
<% else %>
<p class="text-gray-600 mb-4">You are not currently subscribed to any plan.</p>
Expand Down
0