jQuery callback called twice

2019-07-20 15:56发布

问题:

I'm following a Rails cast to implement subscription billing. It's here:

http://railscasts.com/episodes/288-billing-with-stripe

My code is virtually identical, though I pass some additional fields to Stripe. The trouble is, the error message in the javascript ALWAYS displays the error message when the submit button is hit... even on successful charges. I'm not sure if Strip is returning something that triggers the error, or if there's a JS problem.

jQuery ->
  Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'))
  subscription.setupForm()

subscription =
  setupForm: ->
    $('#new_membership').submit ->
      $('input[type=submit]').attr('disabled', true)
      if $('#card_number').length
        subscription.processCard()
        false
      else
        true

  processCard: ->
    card =
      number: $('#card_number').val()
      cvc: $('#card_code').val()
      expMonth: $('#card_month').val()
      expYear: $('#card_year').val()
    Stripe.createToken(card, subscription.handleStripeResponse)

  handleStripeResponse: (status, response) ->
    if status == 200
      $('#membership_stripe_card_token').val(response.id)
      $('#new_membership')[0].submit()
    else
      $('#stripe_error').text(response.error.message)
      $('input[type=submit]').attr('disabled', false)

This is the example stripe gives for error handling. They flip the process and test for errors first:

function stripeResponseHandler(status, response) {
    if (response.error) {
        ...
        //show the errors on the form
        $(".payment-errors").html(response.error.message);
    } else {
        var form$ = $("#payment-form");
        // token contains id, last4, and card type
        var token = response['id'];
        // insert the token into the form so it gets submitted to the server
        form$.append("<input type='hidden' name='stripeToken' value='" + token + "'/>");
        // and submit
        form$.get(0).submit();
    }
}

UPDATE: So I can tell you what's happening, but not exactly why or how to fix it.

I added a console log statement and can see now that handleStripeResponse is being called twice, once when the user hits submit, and it returns a 200, then it seems again (maybe because the form then has to be posted to the Rails app for actual processing? and it returns a 0, which kicks up the error message. BUT -- because Rails is now handling the processing server side, the charge goes through.

Here's the compiled JS, if that helps:

(function() {
  var subscription;

  jQuery(function() {
    Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));
    return subscription.setupForm();
  });

  subscription = {
    setupForm: function() {
      return $('#new_membership').submit(function() {
        $('input[type=submit]').attr('disabled', true);
        if ($('#card_number').length) {
          subscription.processCard();
          return false;
        } else {
          return true;
        }
      });
    },
    processCard: function() {
      var card;
      card = {
        number: $('#card_number').val(),
        cvc: $('#card_code').val(),
        expMonth: $('#card_month').val(),
        expYear: $('#card_year').val()
      };
      return Stripe.createToken(card, subscription.handleStripeResponse);
    },
    handleStripeResponse: function(status, response) {
      if (status === 200) {
        $('#membership_stripe_card_token').val(response.id);
        return $('#new_membership')[0].submit();
      } else {
        $('#stripe_error').text(response.error.message);
        return $('input[type=submit]').attr('disabled', false);
      }
    }
  };

}).call(this);

回答1:

I was having the same problem. I was unknowingly calling the javascript twice.

Once (explicitly) in on the page:

= javascript_include_tag 'stripe'

And then again (unintentionally) in the application.js

//= require_tree .

The result is the jQuery document ready function gets called twice which leads to all listeners being setup twice.

Depending on how you have your application setup, you should get rid of one or the other inclusion. For my application it was best to remove the line from my application.js.

Tricky asset pipeline!



回答2:

Had the same problem. Simply took the jQuery -> out of the top of the coffeescript file in which the Stripe code was stored and the double submissions stopped. Got the hint from Nathan Colgate above.



回答3:

So, my hacky solution thus far is to do this, since the second time through the callback it consistently returns a 0.

 else if status == 0
      $('#stripe_error').text('Processing...')

At least that way, the user doesn't get the error message and the submit button isn't reactivated.



回答4:

I experienced the same problem. Still not sure what caused it. The solution I found was to add a different validation to the coffeescript:

subscription =
  setupForm: ->
    $('#new_subscription').submit ->
      $('input[type=submit]').attr('disabled', true)
      if $('#subscription_stripe_card_token').val
        true
      else
        subscription.processCard()
        false

Notice that for the 'if' statement, instead of checking:

if $('#card_number').length

instead I put a check for:

if $('#subscription_stripe_card_token').val

This way, when the form submits the first time, it successfully returns a value for 'stripe_card_token' and therefore it won't submit the form a second time. What I don't know yet is why using the card_number.length method from the railscast was not working. I believe it may be something wrong with the @subscription object not recognizing that it now has a value for stripe_card_token, and therefore not removing the card_number field... but I don't know

<div class="field">  
  <%= f.label :email %>  
  <%= f.text_field :email %>  
</div>    
<% if @subscription.stripe_card_token %>  
  Credit card has been provided  
<% else %>  
  <div class="field">  
    <%= label_tag :card_number, "Credit Card Number " %>  
    <%= text_field_tag :card_number, nil, name: nil %>  

Hope this helps get closer to a final answer here!



回答5:

To whoever is reading this: if you are here having a similar problem, I'll recommend taking a look at "View page source" or equivalent to see if some javascripts are being included twice.

I was following the same Railscast and ran into the same problem. It took me longer than I'm willing to admit to realize, but my problem was copy pasting a line from the railscast notes while sleepy. The cause was the application javascripts being included twice, once with the autogenerated rails include in application.html.erb:

<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>

and a second time copy pasting the line from railscast notes:

<%= javascript_include_tag "https://js.stripe.com/v1/", "application" %>

therefore including "application" twice. I solved it just by removing one of them.



回答6:

So today at work we had a similar problem:

  • Using Rails 4.2
  • Using ajax form (remote=true) with the built-in jquery-ujs.js script provided with Rails
  • Using Stripe as a payment gateway (they provide a script that through iframe handles some credit card information)
  • When submitting our form (with custom ajax logic) it submits the form twice, once for Ruby's global event form submit handler, and once for "our custom" event handler

Stripe wants to first handle submitting the form, by ajax'ing to Stripe's own domain, and when the result comes back, it wants to fill in some hidden fields in the form with their response data. After this occurs, Stripe wants us to submit the form as normal.

For us there was a conflict in that both Rails and Stripe reacted to the form submission, and didnt coordinate. Since we cant easily change jquery-ujs.js we looked into how they coded their global form "submit" event handler.

It turns out a rails object is exposed as a jQuery plugin, in which theres a function for preventing the jquery-ujs.js handler from handling any event - neat!

So for us the fix was to call...

 $.rails.stopEverything(event);

...inside the Stripe submit event handler (which is a special case for our project)

Here is the code for the Stripe event handler for additional context:

// Create a token or display an error when the form is submitted.
var form = document.getElementById('payment-form');
form.addEventListener('submit', function (event)
{
    $.rails.stopEverything(event); // Dont let Rails' global form submit handler handle this event (in jquery-ujs.js)
    event.preventDefault();

    stripe.createToken(card).then(function(result)
    {
        if (result.error)
        {
            // Inform the user if there was an error
            var errorElement = document.getElementById('card-errors');
            errorElement.textContent = result.error.message;
            $('#stripe-btn').prop('disabled', false)
            $('.fa-spin').remove()
        }
        else
        {
            // Send the token to your server
            stripeTokenHandler(result.token);
        }
    });
});