Adding Stripe Integration to a Ruby on Rails App
Posted 22 May 2024
- ruby
In this transcript, watch how the Navie AI software architect adds a complete working Stripe integration for this application.
👤 USER
Provide implementation details to add a stripe payment process for my existing user setting edit page. This implementation should show that the user is on the "free" tier if they have not paid for the service, and that they are on the "paid" tier if they have paid the $10 one time purchase. This page should allow the user to securely input their credit card information and complete the transaction with stripe.
NAVIE
To add a Stripe payment process to your existing user edit page, you'll need to integrate Stripe in your application. Here's a step-by-step guide to implement this:
- Install the Stripe gem Add the Stripe gem to your Gemfile and bundle install:
- Set Up Stripe Configuration Add your Stripe API keys to your application's credentials or environment variables for better security. Update config/credentials.yml.enc:
- Initialize Stripe Create an initializer config/initializers/stripe.rb:
- Update the User Model Add attributes to track the user's payment status.
- Create a Stripe Checkout Session Add logic in the UsersController to handle the stripe checkout session creation.
- Update the Edit User Page Adding Stripe elements to the user edit page to collect payment:
- Webhook to Handle Payment Create a controller for handling Stripe webhooks. This controller will update the user's payment status.
- Ensure Security and Proper Functioning - Secure the Stripe webhook endpoint using appropriate authentication.
gem 'stripe'
Run the command:
bundle install
stripe_secret_key: your_secret_key
stripe_publishable_key: your_publishable_key
stripe_webhook_secret: your_webhook_secret
Or, use environment variables:
export STRIPE_SECRET_KEY=your_secret_key
export STRIPE_PUBLISHABLE_KEY=your_publishable_key
export STRIPE_WEBHOOK_SECRET=your_webhook_secret
Rails.configuration.stripe = {
publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
secret_key: ENV['STRIPE_SECRET_KEY'],
webhook_secret: ENV['STRIPE_WEBHOOK_SECRET']
}
Stripe.api_key = Rails.configuration.stripe[:secret_key]
rails generate migration AddStripeFieldsToUsers stripe_customer_id:string paid:boolean
rake db:migrate
class UsersController < ApplicationController
...
def edit
@user = User.find(params[:id])
@stripe_session = create_stripe_session(@user)
unless @stripe_session
flash[:error] = "Error creating Stripe session, please try again."
redirect_to edit_user_path(@user)
end
end
private
def create_stripe_session(user)
Stripe::Checkout::Session.create(
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: 'Pro Membership',
},
unit_amount: 1000,
},
quantity: 1,
}],
mode: 'payment',
success_url: root_url,
cancel_url: edit_user_url(user),
)
end
end
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, local: true) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank">change</a>
</div>
<h2>Subscription</h2>
<% if @user.paid? %>
<p>You are on the paid tier.</p>
<% else %>
<p>You are on the free tier.</p>
<button id="checkout-button" class="btn btn-primary">Upgrade to Paid Tier</button>
<% end %>
</div>
</div>
<%= javascript_include_tag 'https://js.stripe.com/v3/' %>
<script type="text/javascript">
var stripe = Stripe('<%= Rails.configuration.stripe[:publishable_key] %>');
document.getElementById("checkout-button").addEventListener("click", function () {
stripe.redirectToCheckout({
sessionId: '<%= @stripe_session.id %>'
}).then(function (result) {
if (result.error) {
alert(result.error.message);
}
});
});
</script>
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def stripe
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, Rails.application.credentials[:stripe][:webhook_secret]
)
rescue JSON::ParserError => e
render status: 400, json: { error: "Invalid payload" }
return
rescue Stripe::SignatureVerificationError => e
render status: 400, json: { error: "Invalid signature" }
return
end
handle_event(event)
render json: { status: "success" }
end
private
def handle_event(event)
case event['type']
when 'checkout.session.completed'
session = event['data']['object']
user = User.find(session.metadata.user_id)
user.update(paid: true)
end
end
end
end
Add the routes for webhook:
Rails.application.routes.draw do
...
post '/webhook/stripe', to: 'webhooks#stripe'
end
- Test the entire flow using Stripe's test mode.
Make sure to adapt values and keys to your environment setup. This is a high-level guide, so you might need to tweak certain parts to align perfectly with your application's setup.
👤 USER
When clicking on the "upgrade to paid tier" button in my user settings edit page, how can i pass the users' email address to the stripe checkout so that the email address gets automatically filled out in the payment window?
NAVIE
To pass the user's email address to the Stripe checkout so that it gets automatically filled out in the payment window, you need to update the Stripe Checkout Session with the user's email passed as a parameter. Here is the updated controller:
class UsersController < ApplicationController
...
private
def create_stripe_session(user)
Stripe::Checkout::Session.create(
payment_method_types: ['card'],
customer_email: user.email, # Pass user's email here
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: 'Pro Membership',
},
unit_amount: 1000,
},
quantity: 1,
}],
mode: 'payment',
success_url: root_url,
cancel_url: edit_user_url(user),
)
end
end
Summary
You will pass the user email address as part of the Stripe Checkout Session creation parameters. This ensures the email field is auto-filled in the payment window for a smoother user experience.👤 USER
After my user successfully pays for the service and the webhooks are processed by my application the user data is not updated with the payment status being successful. Provide an updated implementation to fix this issue so that the user paid status gets updated.
NAVIE
The error indicates that the `session.metadata.user_id` object in your `handle_event` method is missing. Stripe Checkout Sessions must be created with metadata, including the user ID, to later retrieve this information in the webhook event.
To resolve this issue, update your Stripe Checkout Session creation to include the user ID in the metadata and modify your webhook processing code to correctly retrieve the user ID.Step 1: Update Stripe Checkout Session Creation
Ensure that you are passing `user_id` when creating a Stripe Checkout Session:
Step 2: Update the Webhook Processing Code
Modify your `WebhooksController` to correctly access the `user_id` metadata:
Step 3: Ensure Database Migration
Make sure your database has the necessary fields. You already have the migration, but ensure it has been run:
Step 4: Verify and Test
- Verify that the metadata is correctly set when you create a Checkout Session.
- Trigger a payment, and ensure that the webhook payload includes the user ID in the metadata.
- Check if the paid status updates correctly in your database after webhook processing.Testing Webhooks
Use a tool like ngrok to expose your local development server to the internet and test webhooks from Stripe.
Example command: ngrok http 3000
This setup should fix the issue with updating the user's payment status post successful payment through Stripe.
To resolve this issue, update your Stripe Checkout Session creation to include the user ID in the metadata and modify your webhook processing code to correctly retrieve the user ID.
# Assuming this is part of your OrdersController or similar controller
def create_checkout_session
session = Stripe::Checkout::Session.create(
payment_method_types: ['card'],
customer_email: user.email,
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: 'T-shirt',
},
unit_amount: 2000,
},
quantity: 1,
}],
mode: 'payment',
success_url: "#{root_url}success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "#{root_url}cancel",
metadata: {
user_id: current_user.id # Pass the user ID in the metadata
}
)
render json: { session_id: session.id }
end
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def stripe
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
event = nil
webhook_secret = Rails.configuration.stripe[:webhook_secret]
begin
event = Stripe::Webhook.construct_event(payload, sig_header, webhook_secret)
rescue JSON::ParserError
render status: 400, json: { error: "Invalid payload" }
return
rescue Stripe::SignatureVerificationError
render status: 400, json: { error: "Invalid signature" }
return
end
handle_event(event)
render json: { status: "success" }
end
private
def handle_event(event)
case event['type']
when 'checkout.session.completed'
session = event['data']['object']
user_id = session.metadata.user_id
user = User.find(user_id)
user.update(paid: true)
end
end
end
class AddStripeFieldsToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :stripe_customer_id, :string
add_column :users, :paid, :boolean, default: false
end
end
Run the migration with:
rails db:migrate
- Trigger a payment, and ensure that the webhook payload includes the user ID in the metadata.
- Check if the paid status updates correctly in your database after webhook processing.
Example command: ngrok http 3000
This setup should fix the issue with updating the user's payment status post successful payment through Stripe.
👤 USER
Create a series of test cases for this stripe integration so that I can confirm the functionality.
NAVIE
To ensure the Stripe integration is working correctly, you should create test cases that cover various aspects of the payment process, including the creation of Stripe sessions, handling of webhooks, and updating user payment status.
Below are the test cases to cover these areas:
Below are the test cases to cover these areas:
- Unit Test for Webhook Handling Test the webhook handling method to ensure it processes the Stripe event and updates the user's paid status correctly.
- System Test for End-to-End Payment Flow in Profile Edit Test the overall flow from initiating a Stripe checkout session to receiving the webhook and updating the user's payment status.
- Model Test for Stripe Customer Assignment Ensure that a Stripe customer is correctly assigned during user creation.
- Running Tests You can run your tests using the following command:
# test/controllers/webhooks_controller_test.rb
require "test_helper"
class WebhooksControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:michael)
@stripe_event = {
"id" => "evt_123",
"type" => "checkout.session.completed",
"data" => {
"object" => {
"metadata" => {
"user_id" => @user.id
}
}
}
}.to_json
timestamp = Time.now
# Update the signature computation
@signature = Stripe::Webhook::Signature.compute_signature(
timestamp,
@stripe_event,
Rails.configuration.stripe[:webhook_secret]
)
@headers = {
'HTTP_STRIPE_SIGNATURE' => "t=#{timestamp.to_i},v1=#{@signature}",
'Content-Type' => 'application/json'
}
end
test "stripe webhook should update user payment status" do
post webhook_stripe_path, headers: @headers, params: @stripe_event
assert_response :success
@user.reload
assert @user.paid
end
end
# test/system/stripe_payment_flow_test.rb
require "application_system_test_case"
class StripePaymentFlowTest < ApplicationSystemTestCase
def setup
@user = users(:michael)
end
test "end-to-end payment flow" do
log_in_as(@user)
visit edit_user_path(@user)
assert_text "Pro Membership"
# Simulate clicking on the Stripe checkout button and handle payment
# Note: Implement Stripe's mock client for end-to-end testing
# Simulate webhook call
stripe_event = {
"type" => "checkout.session.completed",
"data" => {
"object" => {
"metadata" => {
"user_id" => @user.id
}
}
}
}
signature = Stripe::Webhook::Signature.compute(stripe_event.to_json, Rails.configuration.stripe[:webhook_secret])
post webhook_stripe_path, headers: { 'HTTP_STRIPE_SIGNATURE' => signature }, params: stripe_event.to_json
@user.reload
assert @user.paid
end
end
# test/models/user_test.rb
require "test_helper"
class UserTest < ActiveSupport::TestCase
test "assign stripe customer on user creation" do
user = User.new(name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password")
user.save
assert_not_nil user.stripe_customer_id
end
end
rails test
These test cases cover the essential functionality of the Stripe integration. Remember, these tests assume the existence of user fixtures and that your Stripe keys are set in your test environment. Ensure your `Stripe::Webhook::Signature.compute_signature` function works as intended or use Stripe's testing tools for webhooks.
We have created the following PR with the Navie provided code changes and the Navie crafted PR title and description.