New Stripe SCA checkout flow in Rails

孤街醉人 提交于 2020-07-05 05:12:17

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!