diff --git a/backend/CHANGELOG.md b/backend/CHANGELOG.md
index a9d936dc826..2fddac2a418 100644
--- a/backend/CHANGELOG.md
+++ b/backend/CHANGELOG.md
@@ -29,4 +29,8 @@
* Fixed issue where selecting an existing user in the customer details step would not associate them with an order.
- *Ryan Bigg and dan-ding"
\ No newline at end of file
+ *Ryan Bigg and dan-ding*
+
+* We now use [jQuery.payment](https://stripe.com/blog/jquery-payment) (from Stripe) to provide slightly better formatting on credit card number, expiry and CVV fields.
+
+ *Ryan Bigg*
\ No newline at end of file
diff --git a/backend/app/assets/javascripts/admin/checkouts/edit.js b/backend/app/assets/javascripts/admin/checkouts/edit.js
index 01341ba97c9..5e07b57039c 100644
--- a/backend/app/assets/javascripts/admin/checkouts/edit.js
+++ b/backend/app/assets/javascripts/admin/checkouts/edit.js
@@ -1,8 +1,20 @@
+//= require_self
+//= require jquery.payment
$(document).ready(function() {
if ($('#customer_autocomplete_template').length > 0) {
window.customerTemplate = Handlebars.compile($('#customer_autocomplete_template').text());
}
+ if ($("#card_number").is("*")) {
+ $("#card_number").payment('formatCardNumber')
+ $("#card_expiry").payment('formatCardExpiry')
+ $("#card_code").payment('formatCardCVC')
+
+ $("#card_number").change(function() {
+ $("#cc_type").val($.payment.cardType(this.value))
+ })
+ }
+
formatCustomerResult = function(customer) {
return customerTemplate({
customer: customer,
diff --git a/backend/app/views/spree/admin/payments/source_forms/_gateway.html.erb b/backend/app/views/spree/admin/payments/source_forms/_gateway.html.erb
index 2474790259a..ea08aa73719 100644
--- a/backend/app/views/spree/admin/payments/source_forms/_gateway.html.erb
+++ b/backend/app/views/spree/admin/payments/source_forms/_gateway.html.erb
@@ -29,9 +29,8 @@
diff --git a/core/app/models/spree/credit_card.rb b/core/app/models/spree/credit_card.rb
index cefbefe5faa..8820f299d20 100644
--- a/core/app/models/spree/credit_card.rb
+++ b/core/app/models/spree/credit_card.rb
@@ -3,7 +3,6 @@ class CreditCard < ActiveRecord::Base
has_many :payments, as: :source
before_save :set_last_digits
- after_validation :set_card_type
attr_accessor :number, :verification_value
@@ -14,36 +13,19 @@ class CreditCard < ActiveRecord::Base
scope :with_payment_profile, -> { where('gateway_customer_profile_id IS NOT NULL') }
- def set_last_digits
- number.to_s.gsub!(/\s/,'')
- verification_value.to_s.gsub!(/\s/,'')
- self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
+ def expiry=(expiry)
+ self[:month], self[:year] = expiry.split(" / ")
+ self[:year] = "20" + self[:year]
end
- # cheap hack to get to the type? method from deep within ActiveMerchant
- # without stomping on potentially existing methods in CreditCard
- class CardDetector
- class << self
- include ActiveMerchant::Billing::CreditCardMethods::ClassMethods
- end
+ def number=(num)
+ @number = num.gsub(/[^0-9]/, '') rescue nil
end
- # Some payment gateways, such as USA EPay, only support an ActiveMerchant::Billing::CreditCard
- # object, rather than an object *like* that. So we need to convert it.
- def to_active_merchant
- ActiveMerchant::Billing::CreditCard.new(
- :number => number,
- :month => month,
- :year => year,
- :verification_value => verification_value,
- :first_name => first_name,
- :last_name => last_name
- )
- end
-
- # sets self.cc_type while we still have the card number
- def set_card_type
- self.cc_type ||= CardDetector.brand?(number)
+ def set_last_digits
+ number.to_s.gsub!(/\s/,'')
+ verification_value.to_s.gsub!(/\s/,'')
+ self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
end
def name?
@@ -94,9 +76,15 @@ def has_payment_profile?
gateway_customer_profile_id.present?
end
- def spree_cc_type
- return 'visa' if Rails.env.development?
- cc_type
+ def to_active_merchant
+ ActiveMerchant::Billing::CreditCard.new(
+ :number => number,
+ :month => month,
+ :year => year,
+ :verification_value => verification_value,
+ :first_name => first_name,
+ :last_name => last_name
+ )
end
private
diff --git a/core/spec/models/spree/credit_card_spec.rb b/core/spec/models/spree/credit_card_spec.rb
index 2d855d70449..f7b02a1cb27 100644
--- a/core/spec/models/spree/credit_card_spec.rb
+++ b/core/spec/models/spree/credit_card_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
describe Spree::CreditCard do
-
- let(:valid_credit_card_attributes) { { number: '4111111111111111', verification_value: '123', month: 12, year: 2014 } }
+ let(:valid_credit_card_attributes) { {:number => '4111111111111111', :verification_value => '123', :expiry => "12 / 14"} }
def self.payment_states
Spree::Payment.state_machine.states.keys
@@ -136,49 +135,18 @@ def stub_rails_env(environment)
end
end
- context "#spree_cc_type" do
- before { credit_card.attributes = valid_credit_card_attributes }
-
- context "in development mode" do
- before do
- stub_rails_env("production")
- end
-
- it "should return visa" do
- credit_card.save
- credit_card.spree_cc_type.should == 'visa'
- end
- end
-
- context "in production mode" do
- before { stub_rails_env("production") }
-
- it "should return the actual cc_type for a valid number" do
- credit_card.number = '378282246310005'
- credit_card.save
- credit_card.spree_cc_type.should == 'american_express'
- end
- end
- end
-
- context "#set_card_type" do
- before :each do
- stub_rails_env("production")
- credit_card.attributes = valid_credit_card_attributes
- end
+ context "#number=" do
+ it "should strip non-numeric characters from card input" do
+ credit_card.number = "6011000990139424"
+ credit_card.number.should == "6011000990139424"
- it "stores the credit card type after validation" do
- credit_card.number = '6011000990139424'
- credit_card.save
- credit_card.spree_cc_type.should == 'discover'
+ credit_card.number = " 6011-0009-9013-9424 "
+ credit_card.number.should == "6011000990139424"
end
- it "does not overwrite the credit card type when loaded and saved" do
- credit_card.number = '5105105105105100'
- credit_card.save
- credit_card.number = 'XXXXXXXXXXXX5100'
- credit_card.save
- credit_card.spree_cc_type.should == 'master'
+ it "should not raise an exception on non-string input" do
+ credit_card.number = Hash.new
+ credit_card.number.should be_nil
end
end
diff --git a/core/spec/models/spree/payment_spec.rb b/core/spec/models/spree/payment_spec.rb
index 0d715aa7bfe..30bbb6830ed 100644
--- a/core/spec/models/spree/payment_spec.rb
+++ b/core/spec/models/spree/payment_spec.rb
@@ -557,8 +557,7 @@
it "should build the payment's source" do
params = { :amount => 100, :payment_method => gateway,
:source_attributes => {
- :year => 1.month.from_now.year,
- :month =>1.month.from_now.month,
+ :expiry =>"1 / 99",
:number => '1234567890123',
:verification_value => '123'
}
@@ -570,13 +569,8 @@
end
it "errors when payment source not valid" do
- params = {
- :amount => 100,
- :payment_method => gateway,
- :source_attributes => {
- :year => "2012", :month =>"1"
- }
- }
+ params = { :amount => 100, :payment_method => gateway,
+ :source_attributes => {:expiry => "1 / 12" }}
payment = Spree::Payment.new(params)
payment.should_not be_valid
diff --git a/core/vendor/assets/javascripts/jquery.payment.js b/core/vendor/assets/javascripts/jquery.payment.js
new file mode 100644
index 00000000000..5bc7ef0adb6
--- /dev/null
+++ b/core/vendor/assets/javascripts/jquery.payment.js
@@ -0,0 +1,497 @@
+// Generated by CoffeeScript 1.4.0
+(function() {
+ var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlash, hasTextSelected, luhnCheck, reFormatCardNumber, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, setCardType,
+ __slice = [].slice,
+ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
+ _this = this;
+
+ $ = jQuery;
+
+ $.payment = {};
+
+ $.payment.fn = {};
+
+ $.fn.payment = function() {
+ var args, method;
+ method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+ return $.payment.fn[method].apply(this, args);
+ };
+
+ defaultFormat = /(\d{1,4})/g;
+
+ cards = [
+ {
+ type: 'maestro',
+ pattern: /^(5018|5020|5038|6304|6759|676[1-3])/,
+ format: defaultFormat,
+ length: [12, 13, 14, 15, 16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'dinersclub',
+ pattern: /^(36|38|30[0-5])/,
+ format: defaultFormat,
+ length: [14],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'laser',
+ pattern: /^(6706|6771|6709)/,
+ format: defaultFormat,
+ length: [16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'jcb',
+ pattern: /^35/,
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'unionpay',
+ pattern: /^62/,
+ format: defaultFormat,
+ length: [16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: false
+ }, {
+ type: 'discover',
+ pattern: /^(6011|65|64[4-9]|622)/,
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'mastercard',
+ pattern: /^5[1-5]/,
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'amex',
+ pattern: /^3[47]/,
+ format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
+ length: [15],
+ cvcLength: [3, 4],
+ luhn: true
+ }, {
+ type: 'visa',
+ pattern: /^4/,
+ format: defaultFormat,
+ length: [13, 14, 15, 16],
+ cvcLength: [3],
+ luhn: true
+ }
+ ];
+
+ cardFromNumber = function(num) {
+ var card, _i, _len;
+ num = (num + '').replace(/\D/g, '');
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ if (card.pattern.test(num)) {
+ return card;
+ }
+ }
+ };
+
+ cardFromType = function(type) {
+ var card, _i, _len;
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ if (card.type === type) {
+ return card;
+ }
+ }
+ };
+
+ luhnCheck = function(num) {
+ var digit, digits, odd, sum, _i, _len;
+ odd = true;
+ sum = 0;
+ digits = (num + '').split('').reverse();
+ for (_i = 0, _len = digits.length; _i < _len; _i++) {
+ digit = digits[_i];
+ digit = parseInt(digit, 10);
+ if ((odd = !odd)) {
+ digit *= 2;
+ }
+ if (digit > 9) {
+ digit -= 9;
+ }
+ sum += digit;
+ }
+ return sum % 10 === 0;
+ };
+
+ hasTextSelected = function($target) {
+ var _ref;
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {
+ return true;
+ }
+ if (typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? typeof _ref.createRange === "function" ? _ref.createRange().text : void 0 : void 0 : void 0) {
+ return true;
+ }
+ return false;
+ };
+
+ reFormatCardNumber = function(e) {
+ var _this = this;
+ return setTimeout(function() {
+ var $target, value;
+ $target = $(e.currentTarget);
+ value = $target.val();
+ value = $.payment.formatCardNumber(value);
+ return $target.val(value);
+ });
+ };
+
+ formatCardNumber = function(e) {
+ var $target, card, digit, length, re, upperLength, value;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ value = $target.val();
+ card = cardFromNumber(value + digit);
+ length = (value.replace(/\D/g, '') + digit).length;
+ upperLength = 16;
+ if (card) {
+ upperLength = card.length[card.length.length - 1];
+ }
+ if (length >= upperLength) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (card && card.type === 'amex') {
+ re = /^(\d{4}|\d{4}\s\d{6})$/;
+ } else {
+ re = /(?:^|\s)(\d{4})$/;
+ }
+ if (re.test(value)) {
+ e.preventDefault();
+ return $target.val(value + ' ' + digit);
+ } else if (re.test(value + digit)) {
+ e.preventDefault();
+ return $target.val(value + digit + ' ');
+ }
+ };
+
+ formatBackCardNumber = function(e) {
+ var $target, value;
+ $target = $(e.currentTarget);
+ value = $target.val();
+ if (e.meta) {
+ return;
+ }
+ if (e.which !== 8) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (/\d\s$/.test(value)) {
+ e.preventDefault();
+ return $target.val(value.replace(/\d\s$/, ''));
+ } else if (/\s\d?$/.test(value)) {
+ e.preventDefault();
+ return $target.val(value.replace(/\s\d?$/, ''));
+ }
+ };
+
+ formatExpiry = function(e) {
+ var $target, digit, val;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val() + digit;
+ if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
+ e.preventDefault();
+ return $target.val("0" + val + " / ");
+ } else if (/^\d\d$/.test(val)) {
+ e.preventDefault();
+ return $target.val("" + val + " / ");
+ }
+ };
+
+ formatForwardExpiry = function(e) {
+ var $target, digit, val;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val();
+ if (/^\d\d$/.test(val)) {
+ return $target.val("" + val + " / ");
+ }
+ };
+
+ formatForwardSlash = function(e) {
+ var $target, slash, val;
+ slash = String.fromCharCode(e.which);
+ if (slash !== '/') {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val();
+ if (/^\d$/.test(val) && val !== '0') {
+ return $target.val("0" + val + " / ");
+ }
+ };
+
+ formatBackExpiry = function(e) {
+ var $target, value;
+ if (e.meta) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ value = $target.val();
+ if (e.which !== 8) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (/\d(\s|\/)+$/.test(value)) {
+ e.preventDefault();
+ return $target.val(value.replace(/\d(\s|\/)*$/, ''));
+ } else if (/\s\/\s?\d?$/.test(value)) {
+ e.preventDefault();
+ return $target.val(value.replace(/\s\/\s?\d?$/, ''));
+ }
+ };
+
+ restrictNumeric = function(e) {
+ var input;
+ if (e.metaKey || e.ctrlKey) {
+ return true;
+ }
+ if (e.which === 32) {
+ return false;
+ }
+ if (e.which === 0) {
+ return true;
+ }
+ if (e.which < 33) {
+ return true;
+ }
+ input = String.fromCharCode(e.which);
+ return !!/[\d\s]/.test(input);
+ };
+
+ restrictCardNumber = function(e) {
+ var $target, card, digit, value;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ value = ($target.val() + digit).replace(/\D/g, '');
+ card = cardFromNumber(value);
+ if (card) {
+ return value.length <= card.length[card.length.length - 1];
+ } else {
+ return value.length <= 16;
+ }
+ };
+
+ restrictExpiry = function(e) {
+ var $target, digit, value;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ value = $target.val() + digit;
+ value = value.replace(/\D/g, '');
+ if (value.length > 6) {
+ return false;
+ }
+ };
+
+ restrictCVC = function(e) {
+ var $target, digit, val;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ val = $target.val() + digit;
+ return val.length <= 4;
+ };
+
+ setCardType = function(e) {
+ var $target, allTypes, card, cardType, val;
+ $target = $(e.currentTarget);
+ val = $target.val();
+ cardType = $.payment.cardType(val) || 'unknown';
+ if (!$target.hasClass(cardType)) {
+ allTypes = (function() {
+ var _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ _results.push(card.type);
+ }
+ return _results;
+ })();
+ $target.removeClass('unknown');
+ $target.removeClass(allTypes.join(' '));
+ $target.addClass(cardType);
+ $target.toggleClass('identified', cardType !== 'unknown');
+ return $target.trigger('payment.cardType', cardType);
+ }
+ };
+
+ $.payment.fn.formatCardCVC = function() {
+ this.payment('restrictNumeric');
+ this.on('keypress', restrictCVC);
+ return this;
+ };
+
+ $.payment.fn.formatCardExpiry = function() {
+ this.payment('restrictNumeric');
+ this.on('keypress', restrictExpiry);
+ this.on('keypress', formatExpiry);
+ this.on('keypress', formatForwardSlash);
+ this.on('keypress', formatForwardExpiry);
+ this.on('keydown', formatBackExpiry);
+ return this;
+ };
+
+ $.payment.fn.formatCardNumber = function() {
+ this.payment('restrictNumeric');
+ this.on('keypress', restrictCardNumber);
+ this.on('keypress', formatCardNumber);
+ this.on('keydown', formatBackCardNumber);
+ this.on('keyup', setCardType);
+ this.on('paste', reFormatCardNumber);
+ return this;
+ };
+
+ $.payment.fn.restrictNumeric = function() {
+ this.on('keypress', restrictNumeric);
+ return this;
+ };
+
+ $.payment.fn.cardExpiryVal = function() {
+ return $.payment.cardExpiryVal($(this).val());
+ };
+
+ $.payment.cardExpiryVal = function(value) {
+ var month, prefix, year, _ref;
+ value = value.replace(/\s/g, '');
+ _ref = value.split('/', 2), month = _ref[0], year = _ref[1];
+ if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
+ prefix = (new Date).getFullYear();
+ prefix = prefix.toString().slice(0, 2);
+ year = prefix + year;
+ }
+ month = parseInt(month, 10);
+ year = parseInt(year, 10);
+ return {
+ month: month,
+ year: year
+ };
+ };
+
+ $.payment.validateCardNumber = function(num) {
+ var card, _ref;
+ num = (num + '').replace(/\s+|-/g, '');
+ if (!/^\d+$/.test(num)) {
+ return false;
+ }
+ card = cardFromNumber(num);
+ if (!card) {
+ return false;
+ }
+ return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));
+ };
+
+ $.payment.validateCardExpiry = function(month, year) {
+ var currentTime, expiry, prefix, _ref;
+ if (typeof month === 'object' && 'month' in month) {
+ _ref = month, month = _ref.month, year = _ref.year;
+ }
+ if (!(month && year)) {
+ return false;
+ }
+ month = $.trim(month);
+ year = $.trim(year);
+ if (!/^\d+$/.test(month)) {
+ return false;
+ }
+ if (!/^\d+$/.test(year)) {
+ return false;
+ }
+ if (!(parseInt(month, 10) <= 12)) {
+ return false;
+ }
+ if (year.length === 2) {
+ prefix = (new Date).getFullYear();
+ prefix = prefix.toString().slice(0, 2);
+ year = prefix + year;
+ }
+ expiry = new Date(year, month);
+ currentTime = new Date;
+ expiry.setMonth(expiry.getMonth() - 1);
+ expiry.setMonth(expiry.getMonth() + 1, 1);
+ return expiry > currentTime;
+ };
+
+ $.payment.validateCardCVC = function(cvc, type) {
+ var _ref, _ref1;
+ cvc = $.trim(cvc);
+ if (!/^\d+$/.test(cvc)) {
+ return false;
+ }
+ if (type) {
+ return _ref = cvc.length, __indexOf.call((_ref1 = cardFromType(type)) != null ? _ref1.cvcLength : void 0, _ref) >= 0;
+ } else {
+ return cvc.length >= 3 && cvc.length <= 4;
+ }
+ };
+
+ $.payment.cardType = function(num) {
+ var _ref;
+ if (!num) {
+ return null;
+ }
+ return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;
+ };
+
+ $.payment.formatCardNumber = function(num) {
+ var card, groups, upperLength, _ref;
+ card = cardFromNumber(num);
+ if (!card) {
+ return num;
+ }
+ upperLength = card.length[card.length.length - 1];
+ num = num.replace(/\D/g, '');
+ num = num.slice(0, +upperLength + 1 || 9e9);
+ if (card.format.global) {
+ return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;
+ } else {
+ groups = card.format.exec(num);
+ if (groups != null) {
+ groups.shift();
+ }
+ return groups != null ? groups.join(' ') : void 0;
+ }
+ };
+
+}).call(this);
\ No newline at end of file
diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md
index 1abad6336c0..f7d8c393398 100644
--- a/frontend/CHANGELOG.md
+++ b/frontend/CHANGELOG.md
@@ -8,3 +8,7 @@
* Switch to new Google Analytics analytics.js SDK from ga.js SDK for custom dimensions & metrics.
*Jeff Dutil*
+
+* We now use [jQuery.payment](https://stripe.com/blog/jquery-payment) (from Stripe) to provide slightly better formatting on credit card number, expiry and CVV fields.
+
+ *Ryan Bigg*
\ No newline at end of file
diff --git a/frontend/app/assets/javascripts/store/checkout.js.coffee b/frontend/app/assets/javascripts/store/checkout.js.coffee
index 6a6ac1943cd..b1246201189 100644
--- a/frontend/app/assets/javascripts/store/checkout.js.coffee
+++ b/frontend/app/assets/javascripts/store/checkout.js.coffee
@@ -1,9 +1,18 @@
+//= require jquery.payment
+
Spree.disableSaveOnClick = ->
($ 'form.edit_order').submit ->
($ this).find(':submit, :image').attr('disabled', true).removeClass('primary').addClass 'disabled'
Spree.ready ($) ->
Spree.Checkout = {}
+ $("#card_number").payment('formatCardNumber')
+ $("#card_expiry").payment('formatCardExpiry')
+ $("#card_code").payment('formatCardCVC')
+
+ $("#card_number").change ->
+ $("#cc_type").val($.payment.cardType(@value))
+
if ($ '#checkout_form_address').is('*')
($ '#checkout_form_address').validate()
diff --git a/frontend/app/views/spree/checkout/payment/_gateway.html.erb b/frontend/app/views/spree/checkout/payment/_gateway.html.erb
index 67042a056d3..a36f6eb093c 100644
--- a/frontend/app/views/spree/checkout/payment/_gateway.html.erb
+++ b/frontend/app/views/spree/checkout/payment/_gateway.html.erb
@@ -13,14 +13,16 @@
- <%= label_tag "card_month", Spree.t(:expiration) %>*
- <%= select_month(Date.today, { :prefix => param_prefix, :field_name => 'month', :use_month_numbers => true }, :class => 'required', :id => "card_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") %>
+ <%= label_tag "card_month", t(:expiration) %>*
+ <%= text_field_tag "#{param_prefix}[expiry]", '', :id => 'card_expiry', :class => "required", :placeholder => "MM / YY" %>
<%= label_tag "card_code", Spree.t(:card_code) %>*
<%= text_field_tag "#{param_prefix}[verification_value]", '', options_hash.merge(:id => 'card_code', :class => 'required', :size => 5) %>
<%= link_to "(#{Spree.t(:what_is_this)})", spree.content_path('cvv'), :target => '_blank', "data-hook" => "cvv_link", :id => "cvv_link" %>
+
+<%= hidden_field_tag "#{param_prefix}[cc_type]", '', :id => "cc_type" %>
+
<%= hidden_field param_prefix, 'first_name', :value => @order.billing_firstname %>
<%= hidden_field param_prefix, 'last_name', :value => @order.billing_lastname %>