payment_info_rails 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/jquery.mask.js +489 -0
- data/app/assets/javascripts/modernizr.js +4 -0
- data/app/assets/javascripts/payment_info.js +514 -0
- data/app/assets/javascripts/payment_info_initializer.js +3 -0
- data/app/assets/javascripts/payment_info_main.js +1 -0
- data/app/assets/stylesheets/normalize.css +502 -0
- data/app/assets/stylesheets/payment_info.css +394 -0
- data/app/assets/stylesheets/payment_info_main.css +8 -0
- data/lib/payment_info_rails/engine.rb +4 -0
- data/lib/payment_info_rails/version.rb +3 -0
- data/lib/payment_info_rails.rb +6 -0
- data/payment_info_rails.gemspec +23 -0
- metadata +90 -0
@@ -0,0 +1,514 @@
|
|
1
|
+
// Payment Info Component
|
2
|
+
// Author: Zachary Forrest, modified by Nora Alvarado
|
3
|
+
// Requires: jQuery, Modernizer
|
4
|
+
|
5
|
+
(function ($) {
|
6
|
+
|
7
|
+
"use strict";
|
8
|
+
|
9
|
+
$.fn.paymentInfo = function (method) {
|
10
|
+
|
11
|
+
// Global variables.
|
12
|
+
var methods,
|
13
|
+
helpers,
|
14
|
+
events,
|
15
|
+
ccDefinitions,
|
16
|
+
opts,
|
17
|
+
pluginName = "paymentInfo";
|
18
|
+
|
19
|
+
// Events map for the matchNumbers() function.
|
20
|
+
events = $.map(['change', 'blur', 'keyup', 'keypress', 'keydown'], function (v) {
|
21
|
+
return v + '.' + pluginName;
|
22
|
+
}).join(' ');
|
23
|
+
|
24
|
+
// Credit card regex patterns.
|
25
|
+
ccDefinitions = {
|
26
|
+
'visa': /^4/,
|
27
|
+
'mc': /^5[1-5]/,
|
28
|
+
'amex': /^3(4|7)/,
|
29
|
+
'disc': /^6011/
|
30
|
+
};
|
31
|
+
|
32
|
+
helpers = {
|
33
|
+
|
34
|
+
// Determine if the number being give corresponds to a credit card
|
35
|
+
// regex pattern. If it does, break the loop and return the credit card type.
|
36
|
+
|
37
|
+
getCreditCardType: function (number) {
|
38
|
+
|
39
|
+
var ccType;
|
40
|
+
|
41
|
+
$.each(ccDefinitions, function (i, v) {
|
42
|
+
|
43
|
+
if (v.test(number)) {
|
44
|
+
ccType = i;
|
45
|
+
return false;
|
46
|
+
}
|
47
|
+
|
48
|
+
});
|
49
|
+
|
50
|
+
return ccType;
|
51
|
+
|
52
|
+
},
|
53
|
+
|
54
|
+
// Our matchNumbers function. Probably does more than it should.
|
55
|
+
// Will revisit.
|
56
|
+
|
57
|
+
matchNumbers: function (el) {
|
58
|
+
|
59
|
+
// Check to see if the the credit card number stored in data(). If not,
|
60
|
+
// grab the current value of the credit card field and then get the type
|
61
|
+
// of the card.
|
62
|
+
|
63
|
+
var cardNumber = el.data("ccNumber") || el.val(),
|
64
|
+
ccType = helpers.getCreditCardType(cardNumber);
|
65
|
+
|
66
|
+
// If the credit card field has a value and we know what type of card it is,
|
67
|
+
// then add the appropriate class to the card image <span>. This will allow us
|
68
|
+
// to see the correct card. If we don't know the type, then revert back to the
|
69
|
+
// default image. If the credit card field doesn't have a value, then we revert
|
70
|
+
// back to the default image and erase any credit card number we might have stored.
|
71
|
+
|
72
|
+
if (el.val() !== "") {
|
73
|
+
if (ccType !== "") {
|
74
|
+
$("." + opts.cardImageClass)
|
75
|
+
.addClass(ccType);
|
76
|
+
} else {
|
77
|
+
$("." + opts.cardImageClass)
|
78
|
+
.removeClass()
|
79
|
+
.addClass(opts.cardImageClass);
|
80
|
+
}
|
81
|
+
} else {
|
82
|
+
$("." + opts.cardImageClass)
|
83
|
+
.removeClass()
|
84
|
+
.addClass(opts.cardImageClass)
|
85
|
+
.data("ccNumber", "");
|
86
|
+
}
|
87
|
+
|
88
|
+
// Check the card type to see if it's an Amex. If it is, update the mask to reflect
|
89
|
+
// the number groupings that Amex uses. Also update the CVV. Amex requires four digits
|
90
|
+
// instead of three.
|
91
|
+
|
92
|
+
if (ccType === "amex") {
|
93
|
+
el.mask("0000 000000 00000", { onComplete: helpers.creditCardComplete });
|
94
|
+
$("." + opts.cardCvvClass).mask("000", { onComplete: helpers.cvvComplete });
|
95
|
+
} else {
|
96
|
+
el.mask("0000 0000 0000 0000", { onComplete: helpers.creditCardComplete });
|
97
|
+
$("." + opts.cardCvvClass).mask("000", { onComplete: helpers.cvvComplete });
|
98
|
+
}
|
99
|
+
|
100
|
+
// If the credit card value ever becomes empty, make sure the corresponding expiration date,
|
101
|
+
// CVV, and Zip Code values are also empty.
|
102
|
+
|
103
|
+
if (el.val() === "") {
|
104
|
+
$("." + opts.fieldsetClass)
|
105
|
+
.find("input:gt(0)")
|
106
|
+
.val("");
|
107
|
+
}
|
108
|
+
|
109
|
+
if (ccType !== undefined) {
|
110
|
+
$(el)
|
111
|
+
.parents("." + opts.fieldsetClass)
|
112
|
+
.removeClass("invalid shake");
|
113
|
+
|
114
|
+
$('.' + opts.cardInstructionClass).removeClass("invalid");
|
115
|
+
}
|
116
|
+
|
117
|
+
},
|
118
|
+
|
119
|
+
creditCardComplete: function () {
|
120
|
+
|
121
|
+
// We need to get the credit card field and the unmasked value of the field.
|
122
|
+
var element = $("." + opts.cardNumberClass),
|
123
|
+
uvalue = element.cleanVal(),
|
124
|
+
ccType = helpers.getCreditCardType(uvalue);
|
125
|
+
|
126
|
+
// Let's make sure the card is valid
|
127
|
+
if (ccType === undefined) {
|
128
|
+
$(element)
|
129
|
+
.parents("." + opts.fieldsetClass)
|
130
|
+
.addClass("invalid shake");
|
131
|
+
|
132
|
+
// Update instruction class to reflect the error
|
133
|
+
$('.' + opts.cardInstructionClass).addClass("invalid");
|
134
|
+
|
135
|
+
// Update instruction message
|
136
|
+
helpers.updateInstruction(opts.messageCardNumberError);
|
137
|
+
|
138
|
+
return;
|
139
|
+
}
|
140
|
+
|
141
|
+
// Let's make sure the number entered checks against the Luhn Algorithm
|
142
|
+
if (helpers.ccLuhnCheck(uvalue) === false) {
|
143
|
+
$(element)
|
144
|
+
.parents("." + opts.fieldsetClass)
|
145
|
+
.addClass("invalid shake");
|
146
|
+
|
147
|
+
// Update instruction class to reflect the error
|
148
|
+
$('.' + opts.cardInstructionClass).addClass("invalid");
|
149
|
+
|
150
|
+
// Update instruction message
|
151
|
+
helpers.updateInstruction(opts.messageLuhnCheckError);
|
152
|
+
|
153
|
+
return;
|
154
|
+
}
|
155
|
+
|
156
|
+
// Store the credit card value in data(). Replace the value with the last
|
157
|
+
// four numbers of the card.
|
158
|
+
|
159
|
+
element.bind("saveValues", function () {
|
160
|
+
|
161
|
+
if ((ccType === "amex" && uvalue.length === 15) || (ccType !== "amex" && uvalue.length === 16)) {
|
162
|
+
|
163
|
+
element
|
164
|
+
.data("ccNumber", uvalue)
|
165
|
+
.val(uvalue.substr(uvalue.length - 4, uvalue.length));
|
166
|
+
}
|
167
|
+
|
168
|
+
});
|
169
|
+
|
170
|
+
// Once this function is fired, we need to add a "transitioning" class to credit
|
171
|
+
// card element so that we can take advantage of our CSS animations.
|
172
|
+
element.addClass("transitioning-out");
|
173
|
+
|
174
|
+
// We have to set a timeout so that we give our animations time to finish. We have to
|
175
|
+
// blur the element as well to fix a bug where our credit card field was losing its
|
176
|
+
// value prematurely.
|
177
|
+
|
178
|
+
setTimeout(function () {
|
179
|
+
|
180
|
+
element.removeClass("transitioning-out");
|
181
|
+
|
182
|
+
if (!Modernizr.touch) {
|
183
|
+
|
184
|
+
element.bind("blur", function () {
|
185
|
+
element.trigger("saveValues");
|
186
|
+
}).blur();
|
187
|
+
|
188
|
+
} else {
|
189
|
+
element
|
190
|
+
.trigger("saveValues")
|
191
|
+
.blur(function () {
|
192
|
+
element.trigger("saveValues");
|
193
|
+
});
|
194
|
+
}
|
195
|
+
|
196
|
+
element.addClass("full");
|
197
|
+
|
198
|
+
}, opts.animationWait);
|
199
|
+
|
200
|
+
// Setting another timeout so that we can wait for CSS animations to finish.
|
201
|
+
setTimeout(function () {
|
202
|
+
|
203
|
+
// Expose the rest of the payment info fields now that the credit card
|
204
|
+
// has been filled out.
|
205
|
+
$("." + opts.fieldsetClass)
|
206
|
+
.find("input:gt(0)")
|
207
|
+
.removeClass("hide");
|
208
|
+
|
209
|
+
}, opts.animationWait);
|
210
|
+
|
211
|
+
// After the credit card field is initially filled out, bind a click event
|
212
|
+
// that will allow us to edit the number again if we want to. We also bind
|
213
|
+
// a focus event (for mobile) and a keydown event in case of shift + tab
|
214
|
+
|
215
|
+
$(element)
|
216
|
+
.unbind("blur focus click keydown keypress")
|
217
|
+
.bind("focus click keydown", function (e) {
|
218
|
+
if (e.type === "focus" || e.type === "click" || (e.shiftKey && e.keyCode === 9)) {
|
219
|
+
helpers.beginCreditCard($(element));
|
220
|
+
|
221
|
+
// Update instruction message
|
222
|
+
helpers.updateInstruction(opts.messageEnterCardNumber);
|
223
|
+
}
|
224
|
+
});
|
225
|
+
|
226
|
+
if (window.navigator.standalone || !Modernizr.touch) {
|
227
|
+
// Focus on the credit card expiration input.
|
228
|
+
$("." + opts.cardExpirationClass).focus().val($.trim($("." + opts.cardExpirationClass).val()));
|
229
|
+
|
230
|
+
// Update instruction message
|
231
|
+
helpers.updateInstruction(opts.messageExpiration);
|
232
|
+
}
|
233
|
+
|
234
|
+
},
|
235
|
+
|
236
|
+
// This function is fired when the credit card expiration field's mask has been
|
237
|
+
// satisfied. We trigger an animation for CVV card, wait for it to finish, and then
|
238
|
+
// focus on the CVV field.
|
239
|
+
|
240
|
+
expirationComplete: function () {
|
241
|
+
|
242
|
+
$("." + opts.cardImageClass).addClass("cvv2");
|
243
|
+
|
244
|
+
$("." + opts.cardExpirationClass)
|
245
|
+
.addClass("full")
|
246
|
+
.unbind("keydown blur")
|
247
|
+
.bind("keydown", function (e) {
|
248
|
+
if (e.keyCode === 8 && $(this).val() === "") {
|
249
|
+
$(this).removeClass("full");
|
250
|
+
if (window.navigator.standalone || !Modernizr.touch) {
|
251
|
+
$("." + opts.cardNumberClass).focus();
|
252
|
+
|
253
|
+
// Update instruction message
|
254
|
+
helpers.updateInstruction(opts.messageEnterCardNumber);
|
255
|
+
}
|
256
|
+
}
|
257
|
+
});
|
258
|
+
|
259
|
+
if (window.navigator.standalone || !Modernizr.touch) {
|
260
|
+
setTimeout(function () {
|
261
|
+
$("." + opts.cardCvvClass).focus();
|
262
|
+
|
263
|
+
// Update instruction message
|
264
|
+
helpers.updateInstruction(opts.messageCVV);
|
265
|
+
}, 220);
|
266
|
+
}
|
267
|
+
|
268
|
+
// Update instruction message
|
269
|
+
helpers.updateInstruction(opts.messageCVV);
|
270
|
+
|
271
|
+
},
|
272
|
+
|
273
|
+
// This function is fired when the mask for CVV field is satisfied. We animate
|
274
|
+
// the credit card back from the CVV card image to the appropriate card type.
|
275
|
+
// We wait for the animation to finish and then focus on the zip field.
|
276
|
+
|
277
|
+
cvvComplete: function () {
|
278
|
+
|
279
|
+
$("." + opts.cardImageClass).removeClass("cvv2");
|
280
|
+
|
281
|
+
$("." + opts.cardCvvClass)
|
282
|
+
.addClass("full")
|
283
|
+
.unbind("keydown blur")
|
284
|
+
.bind("keydown", function (e) {
|
285
|
+
if (e.keyCode === 8 && $(this).val() === "") {
|
286
|
+
$(this).removeClass("full");
|
287
|
+
if (window.navigator.standalone || !Modernizr.touch) {
|
288
|
+
$("." + opts.cardExpirationClass).focus();
|
289
|
+
|
290
|
+
// Update instruction message
|
291
|
+
helpers.updateInstruction(opts.messageExpiration);
|
292
|
+
}
|
293
|
+
}
|
294
|
+
});
|
295
|
+
|
296
|
+
if (window.navigator.standalone || !Modernizr.touch) {
|
297
|
+
setTimeout(function () {
|
298
|
+
$("." + opts.cardZipClass).focus();
|
299
|
+
|
300
|
+
// Update instruction message
|
301
|
+
helpers.updateInstruction(opts.messageZip);
|
302
|
+
}, 220);
|
303
|
+
}
|
304
|
+
// Update instruction message
|
305
|
+
helpers.updateInstruction(opts.messageZip);
|
306
|
+
|
307
|
+
},
|
308
|
+
|
309
|
+
zipComplete: function () {
|
310
|
+
|
311
|
+
$("." + opts.cardZipClass)
|
312
|
+
.addClass("full")
|
313
|
+
.unbind("keydown blur")
|
314
|
+
.bind("keydown", function (e) {
|
315
|
+
if (e.keyCode === 8 && $(this).val() === "") {
|
316
|
+
$(this).removeClass("full");
|
317
|
+
if (window.navigator.standalone || !Modernizr.touch) {
|
318
|
+
$("." + opts.cardCvvClass).focus();
|
319
|
+
|
320
|
+
// Update instruction message
|
321
|
+
helpers.updateInstruction(opts.messageCVV);
|
322
|
+
}
|
323
|
+
}
|
324
|
+
})
|
325
|
+
.mask("00000-000", { onComplete: helpers.zipComplete });
|
326
|
+
|
327
|
+
$("." + opts.fieldsetClass)
|
328
|
+
.addClass('valid');
|
329
|
+
|
330
|
+
$("." + opts.cardInstructionClass)
|
331
|
+
.addClass('valid');
|
332
|
+
|
333
|
+
// Update instruction message with success message
|
334
|
+
helpers.updateInstruction(opts.messageSuccess);
|
335
|
+
|
336
|
+
},
|
337
|
+
|
338
|
+
// This function allows us to edit the credit card number once it's been entered.
|
339
|
+
beginCreditCard: function (element) {
|
340
|
+
|
341
|
+
// Set the value of the field to the original card number and apply our
|
342
|
+
// transition state.
|
343
|
+
|
344
|
+
$(element)
|
345
|
+
.val($(element).data("ccNumber"))
|
346
|
+
.addClass("transitioning-in");
|
347
|
+
|
348
|
+
// Wait for the animation to complete and then remove our classes.
|
349
|
+
setTimeout(function () {
|
350
|
+
element.removeClass("transitioning-in full");
|
351
|
+
}, 150);
|
352
|
+
|
353
|
+
// Attach a keypress handler that listens for the "enter" key. If the user
|
354
|
+
// clicks enter, then fire off our creditCardComplete() event.
|
355
|
+
|
356
|
+
$(element)
|
357
|
+
.unbind("keypress blur")
|
358
|
+
.bind("keypress blur", function (e) {
|
359
|
+
|
360
|
+
// Is it the enter key?
|
361
|
+
if (e.keyCode === 13 || e.type === "blur") {
|
362
|
+
|
363
|
+
var uvalue = $(element).cleanVal(),
|
364
|
+
ccType = helpers.getCreditCardType(uvalue);
|
365
|
+
|
366
|
+
// Make sure the number length is valid
|
367
|
+
if ((ccType === "amex" && uvalue.length === 15) || (ccType !== "amex" && uvalue.length === 16)) {
|
368
|
+
helpers.creditCardComplete();
|
369
|
+
}
|
370
|
+
|
371
|
+
}
|
372
|
+
|
373
|
+
})
|
374
|
+
.unbind("focus click keydown");
|
375
|
+
|
376
|
+
// Hide the extraneous inputs until the credit card is filled out again.
|
377
|
+
$("." + opts.fieldsetClass)
|
378
|
+
.find("input:gt(0)")
|
379
|
+
.addClass("hide");
|
380
|
+
|
381
|
+
},
|
382
|
+
|
383
|
+
updateInstruction: function (message) {
|
384
|
+
$('.card-instruction').html(message);
|
385
|
+
},
|
386
|
+
|
387
|
+
// This function returns true or false if
|
388
|
+
/**
|
389
|
+
* Luhn algorithm in JavaScript: validate credit card number supplied as string of numbers
|
390
|
+
* @author ShirtlessKirk. Copyright (c) 2012.
|
391
|
+
* @license WTFPL (http://www.wtfpl.net/txt/copying)
|
392
|
+
*/
|
393
|
+
ccLuhnCheck: (function (arr) {
|
394
|
+
return function (ccNum) {
|
395
|
+
var
|
396
|
+
len = ccNum.length,
|
397
|
+
bit = 1,
|
398
|
+
sum = 0,
|
399
|
+
val;
|
400
|
+
|
401
|
+
while (len) {
|
402
|
+
val = parseInt(ccNum.charAt(--len), 10);
|
403
|
+
sum += (bit ^= 1) ? arr[val] : val;
|
404
|
+
}
|
405
|
+
|
406
|
+
return sum && sum % 10 === 0;
|
407
|
+
};
|
408
|
+
}([0, 2, 4, 6, 8, 1, 3, 5, 7, 9]))
|
409
|
+
|
410
|
+
};
|
411
|
+
|
412
|
+
methods = {
|
413
|
+
|
414
|
+
init: function (options) {
|
415
|
+
|
416
|
+
// Get a copy of our configuration options
|
417
|
+
opts = $.extend({}, $.fn.paymentInfo.defaults, options);
|
418
|
+
|
419
|
+
var zipcodeOptions = {
|
420
|
+
onComplete: helpers.zipComplete,
|
421
|
+
onKeyPress: function(cep, e, field, options) {
|
422
|
+
var masks = ['00000-000', '0-00-00-00'];
|
423
|
+
var mask = (cep.length>7) ? masks[1] : masks[0];
|
424
|
+
$(this).find("." + opts.cardZipClass)
|
425
|
+
.mask(mask, zipcodeOptions)
|
426
|
+
}
|
427
|
+
}
|
428
|
+
|
429
|
+
// Loop through our fieldset, find our inputs and initialize them.
|
430
|
+
return this.each(function () {
|
431
|
+
|
432
|
+
$(this)
|
433
|
+
.find("label")
|
434
|
+
.addClass("hide")
|
435
|
+
.end()
|
436
|
+
.find("." + opts.cardNumberClass)
|
437
|
+
.mask("0000 0000 0000 0000", { onComplete: helpers.creditCardComplete })
|
438
|
+
.before("<span class='" + opts.cardImageClass + "'></span>")
|
439
|
+
.end()
|
440
|
+
.find("." + opts.cardExpirationClass)
|
441
|
+
.mask("00/00", { onComplete: helpers.expirationComplete })
|
442
|
+
.addClass("hide")
|
443
|
+
.end()
|
444
|
+
.find("." + opts.cardCvvClass)
|
445
|
+
.mask("000", { onComplete: helpers.cvvComplete } )
|
446
|
+
.addClass("hide")
|
447
|
+
.focus(function () {
|
448
|
+
$("." + opts.cardImageClass).addClass("cvv2");
|
449
|
+
})
|
450
|
+
.blur(function () {
|
451
|
+
$("." + opts.cardImageClass).removeClass("cvv2");
|
452
|
+
})
|
453
|
+
.end()
|
454
|
+
.find("." + opts.cardZipClass)
|
455
|
+
.mask('00000-000', zipcodeOptions)
|
456
|
+
.addClass("hide")
|
457
|
+
.end();
|
458
|
+
|
459
|
+
if(opts.cardInstruction) {
|
460
|
+
$(this).
|
461
|
+
after("<span class='" + opts.cardInstructionClass + "'>"+ opts.messageEnterCardNumber + "</span>");
|
462
|
+
}
|
463
|
+
|
464
|
+
helpers.matchNumbers($(this).find("." + opts.cardNumberClass));
|
465
|
+
|
466
|
+
}).unbind('.' + pluginName).bind(events, function () {
|
467
|
+
helpers.matchNumbers($(this).find("." + opts.cardNumberClass));
|
468
|
+
});
|
469
|
+
},
|
470
|
+
|
471
|
+
// Need to add this later.
|
472
|
+
destroy: function () {
|
473
|
+
return this.unbind('.' + pluginName);
|
474
|
+
}
|
475
|
+
|
476
|
+
};
|
477
|
+
|
478
|
+
// Plugin methods API
|
479
|
+
if (methods[method]) {
|
480
|
+
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
481
|
+
}
|
482
|
+
if (typeof method === "object" || !method) {
|
483
|
+
return methods.init.apply(this, arguments);
|
484
|
+
}
|
485
|
+
return $.error("Method does not exist in plugin");
|
486
|
+
|
487
|
+
};
|
488
|
+
|
489
|
+
// Plugin config options.
|
490
|
+
$.fn.paymentInfo.defaults = {
|
491
|
+
fieldsetClass: "credit-card-group",
|
492
|
+
cardImageClass: "card-image",
|
493
|
+
cardCvvClass: "card-cvv",
|
494
|
+
cardExpirationClass: "card-expiration",
|
495
|
+
cardZipClass: "card-zip",
|
496
|
+
cardNumberClass: "card-number",
|
497
|
+
cardInstruction : true,
|
498
|
+
cardInstructionClass: "card-instruction",
|
499
|
+
animationWait: 600,
|
500
|
+
focusDelay: 200,
|
501
|
+
messageEnterCardNumber : "Please enter your credit card number",
|
502
|
+
messageCardNumberError : "Please enter a valid credit card number",
|
503
|
+
messageLuhnCheckError : "Please double check your credit card number",
|
504
|
+
messageExpiration : "Please enter your card's expiration month and year",
|
505
|
+
messageExpirationError : "Please enter a valid month and year",
|
506
|
+
messageCVV : "Please enter the three-digit CVV number found on the back of your card",
|
507
|
+
messageCVVAmEx : "Please enter your four-digit CVV number on the front of your card",
|
508
|
+
messageZip : "Please enter your billing zip code",
|
509
|
+
messageSuccess : "Hooray! You've successfully filled out your credit card information."
|
510
|
+
};
|
511
|
+
|
512
|
+
}(jQuery));
|
513
|
+
|
514
|
+
$(".credit-card-group").paymentInfo();
|
@@ -0,0 +1 @@
|
|
1
|
+
//= require_tree .
|