0
votes

I'm integrating Stripe SCA with payment intents into my rails 5.2.3 (ruby 2.5.1) app. I successfully had one-time payments working properly, however after I integrated subscriptions (successfully working), the one-time payments are receiving an incomplete status on Stripe "The customer has not entered their payment method". Looking at the JSON I can see my payment intent with ID successfully being created, however my charges, data is showing up as null. I can't figure out why the data is not being passed to stripe. Here are the corresponding files:

purchases.show.html.erb
<div class="container">
    <h1>Purchasing <%= @recipe.title %> for <%= number_to_currency(@recipe.price) %></h1>
    <%= form_with url: recipe_purchase_path(@recipe.id), local: true, id: "payment-form", data: { payment_intent_id: @payment_intent.client_secret } do |form| %>
        <div class="form-group">
            <label for="card-element">
                Credit or debit card
            </label>

            <div id="card-element" class="form-control">
            </div>

            <div id="card-errors" role="alert">
            </div>
        </div>

        <div class="form-group">
            <label>Name on Card</label>
            <%= form.text_field :name_on_card, placeholder: "Full name", class: "form-control" %>
        </div>

        <div class="form-group">
            <%= form.hidden_field :payment_intent_id, value: @payment_intent.id %>
            <button class="btn btn-outline-primary buy-recipe">Submit Payment</button>  
        </div>

    <% end %>
</div>
purchases_controller.rb
class PurchasesController < ApplicationController
    before_action :authenticate_user!
    before_action :set_recipe, only:[:show, :create]

    def receipt
        @purchase = Purchase.find_by_uuid(params[:id])
        @recipe = Recipe.find(@purchase.recipe_id)
    end

    def show
        @payment_intent = Stripe::PaymentIntent.create(
                amount: @recipe.price_in_cents,
                currency: 'usd',
                payment_method_types: params['card'],
                metadata: {integration_check: 'accept_a_payment'},
            )
    end

    def create
        @payment_intent = Stripe::PaymentIntent.retrieve(params[:payment_intent_id])
        if @payment_intent.status == "succeeded"
            charge = @payment_intent.charges.data.first
            card = charge.payment_method_details.card

            purchase = Purchase.create(
                    customer_id: charge.id,
                    user_id: current_user.id,
                    recipe_id: @recipe.id,
                    uuid:   SecureRandom.uuid,
                    amount: @recipe.price
                )
            current_user.favorites << @recipe
            redirect_to recipe_path(@recipe.slug), notice: "#{@recipe.title} has been added to your Cookbook, thanks for purchasing!"
        else
            flash[:alert] = "Your order was unsuccessful.  Please try again."
            redirect_to recipe_purchase_path(@recipe.id)
        end
    end

    private

    def set_recipe
        @recipe = Recipe.find(params[:recipe_id])
    end

end
purchases.index.js

document.addEventListener("turbolinks:load", () => {
    const form = document.querySelector("#payment-form")
        if (form == null) { return }

    const public_key = document.querySelector("meta[name='stripe-key']").getAttribute("content")
    const stripe = Stripe(public_key)

    const elements = stripe.elements()
    const card = elements.create('card')
    card.mount('#card-element')

    card.addEventListener("change", (event) => {
        var displayError = document.getElementById('card-errors')
        if (event.error) {
            displayError.textContent = event.error.message
        } else {
            displayError.textContent = ''
        }
    })

    form.addEventListener("submit", (event) => {
    event.preventDefault()

    let data = {
      payment_method: {
        card: card,
        billing_details: {
          name: form.querySelector("#name_on_card").value
        }
      }
    }

    stripe.confirmCardPayment(form.dataset.paymentIntentId, data).then((result) => {
      if (result.error) {
        var errorElement = document.getElementById('card-errors')
        errorElement.textContent = result.error.message
      } else {
        // 
        // 
        form.submit()
      }
    })
  })
})

and a screenshot of JSON enter image description here

here is my subscriptions.js file

document.addEventListener("turbolinks:load", () => {
    let cardElement = document.querySelector("#card-element")

    if (cardElement !== null) { setupStripe() }
})

function setupStripe() {
    const stripe_key = document.querySelector("meta[name='stripe-key']").getAttribute("content")
    const stripe = Stripe(stripe_key)

    const elements = stripe.elements()
    const card = elements.create('card')
    card.mount('#card-element')

    var displayError = document.getElementById('card-errors')

    card.addEventListener('change', (event) => {
        if (event.error) {
            displayError.textContent = event.error.message
        } else {
            displayError.textContent = ''
        }
    })

    const form = document.querySelector("#payment-form")
    let paymentIntentId = form.dataset.paymentIntent
    let setupIntentId = form.dataset.setupIntent

    if (paymentIntentId) {
        if (form.dataset.status == "requires_action") {
            stripe.confirmCardPayment(paymentIntentId, { setup_future_usage: 'off_session' }).then((result) => {
                if (result.error) {
                    displayError.textContent = result.error.message
                    form.querySelector("#card-details").classList.remove("d-none")
                } else {
                    form.submit()
                }
            }) 
        }
    }

    form.addEventListener('submit', (event) => {
        event.preventDefault()

        let name = form.querySelector("#name_on_card").value
        let data = {
            payment_method_data: {
                card: card,
                billing_details: {
                    name: name,
                }
            }
        }
        // Complete a payment intent
        if (paymentIntentId) {
            stripe.confirmCardPayment(paymentIntentId, {
                payment_method: data.payment_method_data,
                setup_future_usage: 'off_session',
                save_payment_method: true,
            }).then((result) => {
                if (result.error) {
                    displayError.textContent = result.error.message
                    form.querySelector("#card-details").classList.remove("d-none")
                } else {
                    form.submit()
                }
            })

            // Updating a card or subscribing with a trial (using a SetupIntent)
    } else if (setupIntentId) {
      stripe.confirmCardSetup(setupIntentId, {
        payment_method: data.payment_method_data
      }).then((result) => {
        if (result.error) {
          displayError.textContent = result.error.message
        } else {
          addHiddenField(form, "payment_method_id", result.setupIntent.payment_method)
          form.submit()
        }
      })

        } else {
        //subscribing w no trial
            data.payment_method_data.type = 'card'
            stripe.createPaymentMethod(data.payment_method_data).then((result) => {
                if (result.error) {
                    displayError.textContent = result.error.message
                } else {
                    addHiddenField(form, "payment_method_id", result.paymentMethod.id)
                    form.submit()
                }
            })
        }
    })
}

function addHiddenField(form, name, value) {
    let input = document.createElement("input")
    input.setAttribute("type", "hidden")
    input.setAttribute("name", name)
    input.setAttribute("value", value)
    form.appendChild(input)
}
1
Can you be more precise in isolating your problem? There's a lot going on here and many potential failure points. Are you sure confirmCardPayment is working, or are you hitting the error condition? I notice you're passing in form.dataset.paymentIntentId but earlier you set the client_secret to payment_intent_id. Does the mismatched casing cause issues? Suggest using more precise names to reduce confusion as well. - Nolan H
I believe that is the issue, the confirmCardPayment is not working because the card params are not being sent to Stripe when creating the payment_intent. But I am unsure on where it is failing. - Mark Rybarczyk
Are you hitting if (result.error) { ... } in the callback on confirmCardPayment? If so, what is the error message? You mentioned this was working prior to adding subscription support, but I don't see anything linked to subscriptions here. What changed in this code when you added subscriptions? - Nolan H
I am not seeing that its hitting (result.error) in my logs - Mark Rybarczyk
Sorry, can you clarify the intent behind your purchases and subscriptions js files? They seem to have significant overlap and it's not clear which one we should be focusing on. Again I want to ask you to check your form data. Are you sure confirmCardPayment is even running? Can you log before and after it? It looks like you set data: { payment_intent_id: 'someValue'} in the erb, and then later try to do let paymentIntentId = form.dataset.paymentIntent; if (paymentIntentId) { ... } so I suspect none of this code ever runs. Please add logging to find out where your flow fails. - Nolan H

1 Answers

0
votes

So special thanks to Chris Oliver on this...but what needed to be done was on the show.html.erb I had to change the payment_intent_id to :payment_intent in the form data.

<%= form_with url: recipe_purchase_path(@recipe.id), local: true, id: "payment-form", data: { payment_intent: @payment_intent.client_secret } do |form| %>

then in my show action in purchases_controller.rb I needed to add customer

def show
    @payment_intent = Stripe::PaymentIntent.create(
            amount: @recipe.price_in_cents,
            currency: 'usd',
            payment_method_types: ['card'],
            customer: current_user.stripe_id || current_user.stripe_customer.id
            )
end

Then I completely removed my purchases.js since the one-time payments are being handled in the subscription.js