Stripe.js is a Javascript library that Stripe provides to enable merchants to avoid having credit card information pass through their servers at all. Submitting a form that uses Stripe.js sends a request to Stripe's servers, which respond back with a token that can then be submitted to the merchant's servers and used to authorize and/or capture a payment.
Spree seems currently unable to use this token correctly. I'm using Spree 2.0.5 and spree_gateway branch 2-0-stable from Git. Rails is 3.2.14. I've overridden and modified app/views/spree/checkout/payment/_gateway.html.erb
to work with Stripe.js, as shown here https://stripe.com/docs/stripe.js
I assumed that the Stripe token would be a gateway_payment_profile_id
, and submitted it as that field. However, this generates validation errors on the Payments page: "Payments credit card Number can't be blank" and "Payments credit card Verification Value can't be blank".
These errors are caused by a line in CreditCard:
validates :number, presence: true, unless: :has_payment_profile?, on: :create
#has_payment_profile?
doesn't check whether the gateway_payment_profile_id
exists (as you might think from the name), but instead is defined as:
gateway_customer_profile_id.present?
Changing #has_payment_profile?
to:
gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
allows the CreditCard model to validate and be created, thus taking the order on to confirmation. I can then confirm the order normally, and everything appears work correctly within Spree, and the test charge shows up in Stripe correctly.
I'm not really sure what gateway_customer_profile_id
and gateway_payment_profile_id
really are, as there doesn't appear to be any/much documentation on them. So I'm a bit reluctant to submit a pull request until I understand what these fields are really for, and how this change might affect other things.
I'm not even sure if this type of payment token should be saved as a CreditCard instance, but can't really see anywhere else for it to go.
Is this the right way to do this?
Here is my modified view file:
<%# _gateway.html.erb
# Copied from Spree v 2.0.5
# Edited to work with Stripe.js
%>
<%= image_tag 'credit_cards/credit_card.gif', :id => 'credit-card-image' %>
<% param_prefix = "payment_source[#{payment_method.id}]" %>
<div id='stripe-errors'></div>
<p class="field" data-hook="card_number">
<%= label_tag "card_number", Spree.t(:card_number) %>
<span class="required">*</span><br />
<% options_hash = Rails.env.production? ? {:autocomplete => 'off'} : {} %>
<input id='card_number' class='required' size=19 maxlength=19 autocomplete="off" type="text" data-stripe="number"/>
<span id="card_type" style="display:none;">
( <span id="looks_like" ><%= Spree.t(:card_type_is) %> <span id="type"></span></span>
<span id="unrecognized"><%= Spree.t(:unrecognized_card_type) %></span>
)
</span>
</p>
<p class="field" data-hook="card_expiration">
<%= label_tag "card_month", Spree.t(:expiration) %><span class="required">*</span><br />
<%= select_month(Date.today, { :prefix => param_prefix, :field_name => 'month', :use_month_numbers => true }, :class => 'required', :id => "card_month", :"data-stripe" => 'exp-month') %>
<%= select_year(Date.today, { :prefix => param_prefix, :field_name => 'year', :start_year => Date.today.year, :end_year => Date.today.year + 15 }, :class => 'required', :id => "card_year", :"data-stripe" => 'exp-year') %>
</p>
<p class="field" data-hook="card_code">
<%= label_tag "card_code", Spree.t(:card_code) %><span class="required">*</span><br />
<input id='card_code' class='required' size=5 data-stripe='cvc'/>
<%= link_to "(#{Spree.t(:what_is_this)})", spree.content_path('cvv'), :target => '_blank', "data-hook" => "cvv_link", :id => "cvv_link" %>
</p>
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript">
// This identifies your website in the createToken call below
Stripe.setPublishableKey( "<%= ENV[ Rails.env.production? ? 'STRIPE_PUBLISHABLE_KEY' : 'STRIPE_TEST_PUBLISHABLE_KEY'] %>" );
// ...
</script>
<script type='text/javascript'>
// Handle response from Stripe when we request a payment token
var stripeResponseHandler = function(status, response) {
var $form = $('#<%= "checkout_form_#{@order.state}" %>');
if (response.error) {
// Show the errors on the form
$form.find('#stripe-errors').text(response.error.message);
$form.find('#stripe-errors').addClass('errorExplanation');
$form.find('button').prop('disabled', false);
} else {
// 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="payment_source[1][gateway_payment_profile_id]" />').val(token));
// and submit
$form.get(0).submit();
}
};
// Event handler for form submit button -- create a Stripe payment token
jQuery(function($) {
$('#<%= "checkout_form_#{@order.state}" %>').submit(function(event) {
//alert("submit event handler; key = '<%= ENV[ Rails.env.production? ? 'STRIPE_PUBLISHABLE_KEY' : 'STRIPE_TEST_PUBLISHABLE_KEY'] %>'");
var $form = $(this);
event.preventDefault();
event.stopPropagation();
//alert("submit event handler - should have killed submission");
// Disable the submit button to prevent repeated clicks
$form.find('input.button.continue[type="submit"]').prop('disabled', true);
Stripe.card.createToken($form, stripeResponseHandler);
// Prevent the form from submitting with the default action
return false;
});
});
</script>