0
votes

The problem is that I can't show stripe errors on signup modal dialog.

Javascript:

$.externalScript('https://js.stripe.com/v1/').done(function(script, textStatus) {
  Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));
  var subscription = {
    setupForm: function() {
      return $('form').submit(function() {
        $('input[type=submit]').prop('disabled', true);
        if ($('#card_number').length) {
          subscription.processCard();
          return false;
        } else {
          return true;
        }
      });
    },
    processCard: function() {
      var card;
      card = {
        name: $('#user_name').val(),
        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) {
        $('#stripe_token').val(response.id)
        $('form')[0].submit()
      } else {
        $('#stripe_error').text(response.error.message).show();
        return $('input[type=submit]').prop('disabled', false);
      }
    }
  };
  return subscription.setupForm();

});

In the same js file, I have this ajax call for devise's create method but I get null value for error all the time. So I guess this part below and the one above can't go together. I tried to merge them and do ajax call inside handleStripeResponse function above but then some other problems appeared.

$('.signup').click(function(e){
e.preventDefault();
$.ajax ({
  url: '/users',
  method: 'POST',
  dataType: "JSON",
  success: function(data) {
    if (data.status == 'failed') {
      $('#stripe_error').show().text(data.error_message);
      return false;
    }
  }
});

});

Devise Registrations Controller:

def create
build_resource
user = User.new params[:user]

if user.save_with_payment 
  if user.active_for_authentication?
    set_flash_message :notice, :signed_up if is_navigational_format?
    sign_in(resource_name, user)
    # respond_with resource, :location => after_sign_up_path_for(user)
    render json: { status: 'success', resource: user }
  else
    set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
    expire_session_data_after_sign_in!
    respond_with user, :location => after_inactive_sign_up_path_for(user)
  end
else
  clean_up_passwords user
    render json: { status: 'failed', error_message: user.save_with_payment }
end

end

User model:

def save_with_payment
  if valid?
    stripe_customer = Stripe::Customer.create(
      :email => email,
      :source  => stripe_token
    )
    self.customer_id = stripe_customer.id
    update_stripe_user_details
    save!
  end

  rescue Stripe::InvalidRequestError => e
    logger.error "Stripe error while creating customer: #{e.message}"
    errors.add :base, "There was a problem with your credit card."
  rescue Stripe::CardError => e
    stripe_error e, e.message
    errors.add :base, e.message
  rescue Stripe::InvalidRequestError => e
    stripe_error e, e.message
    errors.add :base, e.message
  rescue Stripe::AuthenticationError => e
    stripe_error e, e.message
    errors.add :base, e.message
  rescue Stripe::APIConnectionError => e
    stripe_error e, e.message
    errors.add :base, e.message
  rescue Stripe::StripeError => e
    stripe_error e, e.message
    errors.add :base, e.message
  rescue => e
    stripe_error e, e.message
    errors.add :base, e.message
end 

Please point me in the right direction, I am getting out of ideas.

1

1 Answers

0
votes

The main problem you have is that you are trying to do way to much in a single controller action.

Your registrations controller is not just responsible for dealing with the users form input but also with dealing with a 3:rd party - if you start to think about the potential number of code paths here you'll realize that this is a crazy amount of responsibilities.

What you might want to do is split the actions up:

POST /registrations # create a user profile
POST /registrations/:registration_id/payments # pay

This gives you a clear set of responsibilities and makes it much easier to allow the user to retry failed payments without starting from square one or having an overly complex setup of passing params back and forth.

If what you are building is a members only type site where you want to restrict access to paying users you should handle this on the authorization layer - not the authentication layer.

What this means in practice is that you let any user with has completed the signup authorize but then check a flag on the record or count the number of payments on the record when you check if the user is authorized to view any content.

class Ability
  include CanCan::Ability
  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.payed_in_full?
      can :read, :all
    end
  end
end

edit

The problem is that at the same form which has one button, I need to sign > up user and enter stripe card details.

Yeah, thats exactly the point. You need to change your approach.

If an ajax solution is acceptable you would do something like this:

function sendForm($form){
  return $.ajax($form.attr('action'), {
    data: $form.serializeArray(),
    method: 'POST',
    dataType: "JSON",
  });
}

$(document).on('.signup', 'submit', function(){
  var $form = $(this);
  var promise = sendForm($form);
  promise.done(function(data, textStatus, jqXHR ){
    if (jqXHR.statusText === "OK") {
       console.log("User signup was successful");
       $form.toggleClass('signup payup');
       $form.attr('action', '/payments');
       $form.submit(); // this will cause the '.payup', 'submit' event
    else {
       console.log("User signup failed");
       // todo - display errors
    }
  });

  return false; // prevent default action
});

$(document).on('.payup', 'submit', function(){
  var $form = $(this);
  // should post to something like `/payments`
  var promise = sendForm($form);
  promise.done(function(data, textStatus, jqXHR ){
    if (jqXHR.statusText === "OK") {
       console.log("Payment was created successfully");
    else {
       console.log("Payment failed");
       // todo - display errors
    }
  });

  return false; // prevent default action
});

This gives you two different HTTP calls and is far simpler from an API standpoint, while the user experience is that he only submits the form once.