【问题标题】:New Stripe SCA checkout flow in RailsRails 中的新 Stripe SCA 结账流程
【发布时间】:2020-01-13 00:54:48
【问题描述】:

我正在努力将我的 Rails 应用程序切换到新的 Stripe 结账流程以适应新的 SCA 法规。

我想实现这个链接中的简单动态产品例程:https://stripe.com/docs/payments/checkout/migration#api-products-after

我不知道在哪里放置不同的代码。应该输入什么: - 控制器 -> 在哪些方法中
- 视图 -> 例如事件显示视图。用户将点击的表单/按钮
- javascript -> 如何传递正确的会话 ID - 再次控制器 -> 实现成功和错误用例

Stripe 技术支持刚刚将我发送到上面的文档链接,因此我非常感谢您在这里提供一些帮助。

【问题讨论】:

    标签: ruby-on-rails psd2 stripe-payments


    【解决方案1】:

    新的 Stripe Checkout 的 Rails 工作流程是:

    • 创建 Stripe Checkout Session 并检索 session.id (.rb)

    • 将 session.id 传递给 js 初始化程序以重定向到 Stripe Checkout

    条纹结帐会话

    这是我用于订阅服务的示例client/server Stripe Checkout implementation。除了引用 Stripe 产品而不是计划之外,您的步骤基本相同:

    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
    

    就我而言,我的index.html.erb 模板有一个指向“获取更多信息...”的链接,以了解特定订阅。该链接转到控制器的 :new 操作,将相关的 Stripe 计划(或产品)信息作为参数传递。在您的情况下,您可以传递 Stripe Checkout 会话所需的任何产品参数:

    subscriptions/index.html.erb
    <%= link_to 'Get more info...', new_subscription_path(plan: 'plan_xxx' %>
    

    :new 控制器操作将返回您的 Stripe CHECKOUT_SESSION_ID 以在您的模板中使用。 (另外,请注意,此控制器绕过了 logged_in? 和伪造保护,以允许 Stripe Webhook POST 响应您的 Checkout Session。您需要在此处解决您的特定授权方案)

    现在,您需要调用 Stripe API。我在这样的 Stripe 服务中这样做:

    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
    

    如果您对 Stripe 的调用成功,控制器中的 session 对象:new 操作现在将包含您的会话数据:

    def new
      session = StripeSession.new_session(STRIPE_API_KEY, current_user.email, params[:plan])
      @stripe_session = session
    end
    

    JS 脚本加载

    您将使用链接中的 session.id 重定向到 Stripe Checkout 页面:

    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>
    

    上面的模板做了几件重要的事情:

    • 加载条带 v3 js 脚本(取决于您如何/在何处加载此脚本。如果使用 content_for,那么您的 layout.html 文件将有一个相应的块:

    &lt;% if content_for? :add_to_head %&gt; &lt;%= yield :add_to_head %&gt; &lt;% end %&gt;

    • 将 @stripe_session.id 从控制器 :new 操作传递到 &lt;div&gt; 元素的 data-stripe-id 属性。

    • 为 subscribe-btn 添加 EventListener 以重定向到 Stripe Checkout,传入 @stripe_session.id

    JS 脚本的另一种方法

    还有其他方法可以加载 js 脚本。就个人而言,我喜欢使用Stimulus 来处理这类事情。例如,我没有使用content_for 加载js 并使用&lt;script&gt; 标签,而是使用subscription_controller.js 刺激控制器来完成这项工作:

    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)
        })
      }
    }
    
    • 您需要在 Rails 应用中添加/初始化 Stimulus 才能使上述功能正常工作...

    STRIPE WEBHOOKS

    Stripe 将 POST 到您的 webhook 端点(如果您将它们配置为)。如果监听它们,您可以配置一些 routes(见下文)来处理它们。您也可以在您选择的服务中执行此操作。例如,在您的 app/services 文件夹中创建另一个文件:

    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
    

    此文件将接收并授权您在 Stripe Dashboard 中配置的传入 Stripe webhook。如果成功,event 属性将包含您当前正在摄取的任何 webhook 的 JSON 响应。

    这允许您基于 event.type 调用各种方法,这将是 webhook 的名称。 event.data.object 将带您进入特定的响应数据。

    铁路路线

    如果没有正确的路线,以上任何一项都不会起作用!

    routes.rb
    get 'success', to: 'subscriptions#success'
    get 'cancel', to: 'subscriptions#cancel'
    resources :subscriptions
    post '/stripe-webhooks', to: 'subscriptions#stripe_webhook'
    

    我必须将 get 'success' 和 'cancel' 路由放在订阅资源上方,以便它们正确解析。

    最后,将 successcancel 回调添加到您的控制器,然后对它们进行任何您需要的操作。例如:

    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
    ...
    

    注意:您需要一个对应的success.html.erb 文件。如果您愿意,取消操作也可以重定向或创建一个 html.erb 文件。

    所以,完成所有设置有点难。然而,随着管道的使用,有很多很酷的可能性来处理各种生命周期事件/webhook。目前,我正在收听其中大约 15 首,以确保我的订阅系统顺利运行。

    祝你好运!

    【讨论】:

    • 非常感谢这个 bnwpro。我被拉到另一个方向,但我很快就会按照你的指示再次处理这个问题。
    • 有什么方法可以一次性做到这一点吗?让用户输入所有详细信息,并在提交按钮上生成会话并重定向到结帐页面?
    【解决方案2】:

    我没有使用 ruby​​,但如果在创建会话时成功结帐完成时传递会话 ID,只需在 * _url 后添加“?session_id={CHECKOUT_SESSION_ID}”, 不知道这是否是您的情况,但很乐意提供帮助

        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}',
    

    另外,我建议看这个https://youtube.com/watch?v=8TNQL9x6Ntg

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-07-04
    • 2017-07-10
    • 2021-08-31
    • 2020-06-26
    • 2021-08-06
    • 1970-01-01
    • 2017-10-28
    • 2021-03-02
    相关资源
    最近更新 更多