问题
I'm struggling with switching my Rails app to the new Stripe checkout flow to accommodate the new SCA regulation.
I want to implement the simple dynamic product routine found in this link: https://stripe.com/docs/payments/checkout/migration#api-products-after
I can't figure out where to put the different pieces of code. What should go in:
- controller -> in which methods
- views -> the event show view for example. The form/button the user will click
- javascript -> how to pass the right session id
- controller again -> implementing the success and error use cases
The Stripe tech support just sent me to the documentation link above so I would really appreciate some help here.
回答1:
The Rails workflow for the new Stripe Checkout is:
Create a Stripe Checkout Session and retrieve the session.id (.rb)
Pass the session.id to a js initializer to redirect to Stripe Checkout
STRIPE CHECKOUT SESSION
This is a sample client/server Stripe Checkout implementation that I'm using for a Subscription service. Your steps would essentially be the same except you would be referencing a Stripe Product rather than a Plan:
subscriptions_controller.rb
STRIPE_API_KEY = Rails.application.credential.stripe[:secret_key]
skip_before_action :user_logged_in?, only: :stripe_webhook
protect_from_forgery except: :stripe_webhook
def stripe_webhook
stripe_response = StripeWebhooks.subscription_events(request)
end
def index
end
def new
session = StripeSession.new_session(STRIPE_API_KEY, current_user.email, params[:plan])
@stripe_session = session
end
In my case, my index.html.erb
template has a link to "Get more info..." about a particular subscription. That link goes to the controller's :new action, passing the relevant Stripe Plan (or Product) info as params. In your case, you might pass whatever Product params necessary for your Stripe Checkout session:
subscriptions/index.html.erb
<%= link_to 'Get more info...', new_subscription_path(plan: 'plan_xxx' %>
The :new controller action will return your Stripe CHECKOUT_SESSION_ID for use in your template. (Also, note that this controller is bypassing logged_in? and forgery protection to allow for the Stripe Webhook POST response to your Checkout Session. You'll need to address your particular authorization scheme here)
Now, you need to call the Stripe API. I'm doing this in a Stripe service like so:
app/services/stripe_session.rb
class StripeSession
require 'stripe' ### make sure gem 'stripe' is in your Gemfile ###
def self.new_session(key, user_email, plan)
new(key, customer_email: user_email, plan: plan).new_checkout_session
end
def initialize(key, options={})
@key = key
@customer_email = options[:customer_email]
@plan = options[:plan]
end
def new_checkout_session
Stripe.api_key = key
session = Stripe::Checkout::Session.create(
customer_email: customer_email,
payment_method_types: ['card'],
subscription_data: {
items: [{
plan: plan,
}],
},
success_url: 'https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://yourapp.com/cancel'
)
end
private
attr_reader :key, :customer_email, :plan
end
If your call to Stripe was successful the session
object in your controller :new action will now contain your session data:
def new
session = StripeSession.new_session(STRIPE_API_KEY, current_user.email, params[:plan])
@stripe_session = session
end
JS SCRIPT LOADING
You'll be using the session.id in your link to redirect to the Stripe Checkout page:
subscriptions/new.html.erb
<%= content_for :header do %>
<script src="https://js.stripe.com/v3/" data-turbolinks-eval="false"></script>
<% end %>
<div data-stripe="<%= @stripe_session.id %>">
<%= link_to 'Subscribe', '', class: 'subscribe-btn', remote: true %>
</div>
<script>
const subscribeBtn = document.querySelector('.subscribe-btn')
subscribeBtn.addEventListener('click', e => {
e.preventDefault()
const CHECKOUT_SESSION_ID = subscribeBtn.parentElement.dataset.stripe
stripe.redirectToCheckout({
sessionId: CHECKOUT_SESSION_ID
}).then((result) => {
// handle any result data you might need
console.log(result.error.message)
})
}
</script>
The above template is doing several important things:
- Load the stripe v3 js script (it's up to you how/where you load this script. If using
content_for
then your layout.html file would have a corresponding block:
<% if content_for? :add_to_head %> <%= yield :add_to_head %> <% end %>
Pass the @stripe_session.id from the controller :new action to the data-stripe-id attribute of your
<div>
element.Add the EventListener for the subscribe-btn to redirect to Stripe Checkout, passing in the @stripe_session.id
ALTERNATE APPROACH FOR JS SCRIPTS
There are other ways to load the js scripts. Personally, I love using Stimulus for this sort of thing. For example, rather than loading js with content_for
and using <script>
tags I have a subscription_controller.js
Stimulus Controller doing the work:
subscriptions/new.html.erb (now becomes)
<div data-controller="subscription" data-session="<%= @stripe_session.id %>">
<%= link_to 'Subscribe', '', class: 'btn', remote: true,
data: {action: 'subscription#redirectToCheckout', target: 'subscription.sessionID'}
%>
</div>
---
(The Stimulus controller)
app/javascript/controllers/subscription_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ 'sessionID' ]
get sessionID() {
return this.sessionIDTarget.parentElement.dataset.session
}
initialize() {
const script = document.createElement('script')
script.src = "https://js.stripe.com/v3/"
document.head.appendChild(script)
}
redirectToCheckout(e) {
e.preventDefault()
// grab your key securely in whichever way works for you
const stripe = Stripe('pk_test_xxx')
const CHECKOUT_SESSION_ID = this.sessionID
stripe.redirectToCheckout({
sessionId: CHECKOUT_SESSION_ID
}).then((result) => {
console.log(result.error.message)
})
}
}
- You would need to add/initialize Stimulus to your Rails app for the above to work...
STRIPE WEBHOOKS
Stripe will POST to your webhook endpoints (if you configure them to). If listening for them, you configure some routes
(see below) to handle them. You can also do this in a service of your choosing. For example, create another file in your app/services folder:
app/services/stripe_webhooks.rb
class StripeWebhooks
require 'stripe'
STRIPE_API_KEY = Rails.application.credentials.stripe[:secret_key]
def self.subscription_events(request)
new(request).subscription_lifecycle_events
end
def initialize(request)
@webhook_request = request
end
def subscription_lifecycle_events
authorize_webhook
case event.type
when 'customer.created'
handle_customer_created
when 'checkout.session.completed'
handle_checkout_session_completed
when # etc.
end
end
private
attr_reader :webhook_request, :event
def handle_customer_created(event)
## your work here
end
def handle_checkout_session_completed(event)
## your work here
end
def authorize_webhook
Stripe.api_key = STRIPE_API_KEY
endpoint_secret = Rails.application.credentials.stripe[:webhooks][:subscription]
payload = webhook_request.body.read
sig_header = webhook_request.env['HTTP_STRIPE_SIGNATURE']
@event = nil
begin
@event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
puts e.message
rescue Stripe::SignatureVerificationError => e
puts e.message
end
end
end
This file will receive and authorize the incoming Stripe webhook that you configured in your Stripe Dashboard. If successful, event
attribute will contain the JSON response of whichever webhook you're ingesting at the moment.
That allows you to call various methods based on the event.type
which will be the name of the webhook. event.data.object
will get you into specific response data.
RAILS ROUTES
None of the above will work without the proper routes!
routes.rb
get 'success', to: 'subscriptions#success'
get 'cancel', to: 'subscriptions#cancel'
resources :subscriptions
post '/stripe-webhooks', to: 'subscriptions#stripe_webhook'
I had to place the get 'success' & 'cancel' routes above the subscription resources for them to resolve properly.
And, finally, add the success
and cancel
callbacks to your controller and do whatever you need with them. For example:
subscriptions_controller.rb
...
def success
### the Stripe {CHECKOUT_SESSION_ID} will be available in params[:session_id]
if params[:session_id]
flash.now[:success] = "Thanks for your Subscribing/Purchasing/Whatever..."
else
flash[:error] = "Session expired error...your implementation will vary"
redirect_to subscriptions_path
end
end
def cancel
redirect_to subscriptions_path
end
...
Note: you'll need a corresponding success.html.erb
file. The cancel action can redirect or create an html.erb file for that too if you'd like.
So, it was kind of a bear to get it all setup. However, with the plumbing out of the way there are lots of cool possibilities to handle all sorts of lifecycle events/webhooks. Currently, I'm listening for about 15 of them to keep my subscription system running smoothly.
Good luck!
回答2:
I'm not using ruby but in the case to pass the session ID when Success checkout is Done when creating the session just add "?session_id={CHECKOUT_SESSION_ID}" after the * _url, Don't know if this is your case but glad to help
mode : "subscription",
customer : customerid,
success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://example.com/cancel?session_id={CHECKOUT_SESSION_ID}',
also, I suggest watching this https://youtube.com/watch?v=8TNQL9x6Ntg
来源:https://stackoverflow.com/questions/57884800/new-stripe-sca-checkout-flow-in-rails