From 48e9208c75706690d80835fcf4b115a8c26ed979 Mon Sep 17 00:00:00 2001 From: borisson_ Date: Sat, 4 Mar 2017 12:18:06 +0000 Subject: [PATCH] Issue #2790551 by steveoliver, bojanz, borisson_: Implement a JS library for the credit card form --- .../payment/commerce_payment.libraries.yml | 7 + modules/payment/js/credit-card-validation.js | 258 ++++++++++++++++++ .../src/PluginForm/PaymentMethodAddForm.php | 2 + 3 files changed, 267 insertions(+) create mode 100644 modules/payment/js/credit-card-validation.js diff --git a/modules/payment/commerce_payment.libraries.yml b/modules/payment/commerce_payment.libraries.yml index 8939ef4c83..246bf90412 100644 --- a/modules/payment/commerce_payment.libraries.yml +++ b/modules/payment/commerce_payment.libraries.yml @@ -1,3 +1,10 @@ +credit_card_validation: + version: VERSION + js: + js/credit-card-validation.js: {} + dependencies: + - core/jquery + payment_method_form: version: VERSION css: diff --git a/modules/payment/js/credit-card-validation.js b/modules/payment/js/credit-card-validation.js new file mode 100644 index 0000000000..842bcae470 --- /dev/null +++ b/modules/payment/js/credit-card-validation.js @@ -0,0 +1,258 @@ +/** + * @file + * Attaches credit card validation logic. + */ + +(function ($) { + + 'use strict'; + + var types = {}; + var VISA = 'visa'; + var MASTERCARD = 'master-card'; + var AMEX = 'amex'; + var DINERSCLUB = 'dinersclub'; + var DISCOVER = 'discover'; + var MAESTRO = 'maestro'; + + types[VISA] = { + niceType: 'Visa', + type: VISA, + pattern: ['4'], + gaps: [4, 8, 12], + lengths: [16] + }; + + types[MASTERCARD] = { + niceType: 'MasterCard', + type: MASTERCARD, + pattern: ['51-55', '222100-272099'], + gaps: [4, 8, 12], + lengths: [16] + }; + + types[AMEX] = { + niceType: 'American Express', + type: AMEX, + pattern: ['34', '37'], + lengths: [15] + }; + + types[DINERSCLUB] = { + niceType: 'Diners Club', + type: DINERSCLUB, + pattern: ['300-305', '309', '36', '38', '39'], + lengths: [14] + }; + + types[DISCOVER] = { + niceType: 'Discover Card', + type: DISCOVER, + pattern: ['6011', '622126-622925', '644-649', '65'], + lengths: [16, 19] + }; + + types[MAESTRO] = { + niceType: 'Maestro', + type: MAESTRO, + pattern: [ + '5018', + '5020', + '5038', + '5612', + '5893', + '6304', + '6759', + '6761', + '6762', + '6763', + '0604', + '6390' + ], + lenghts: [12, 13, 14, 15, 16, 17, 18, 19] + }; + + + /** + * Detect the type of credit card. + * + * @param {string} number + * + * @return {object|boolean} + */ + var detectType = function (number) { + // Loop over all available types. + for (var x in types) { + var type = types[x]; + + // Loop over all patterns in the type. + for (var i in type.pattern) { + var pattern = type.pattern[i]; + + // If the pattern has a dash, we should create a range of patterns. + if (pattern.indexOf('-') >= 0) { + var exploded_pattern; + var ranges = []; + var range; + exploded_pattern = pattern.split('-'); + + while (exploded_pattern[0] <= exploded_pattern[1]) { + ranges.push(exploded_pattern[0]); + exploded_pattern[0]++; + } + + for (range in ranges) { + if (validatePrefix(number, range)) { + return type; + } + } + } + // No dashes, so just validate this pattern. + else if (validatePrefix(number, pattern)) { + return type; + } + } + } + + return false; + }; + + /** + * Validate the prefix is according to the expected prefix. + * + * @param {string} number + * @param {string} prefix + * + * @return {boolean} + */ + var validatePrefix = function (number, prefix) { + return number.substring(0, prefix.length) == prefix + ''; + }; + + /** + * Validate credit card. + * + * @param {string} number + * @param {object} type + * + * @return {boolean} + */ + var validateCreditCard = function (number, type) { + // Make sure that the type is really the expected type. + if (detectType(number) != type) { + return false; + } + + // Test that the length of the card is actually one of the expected lengths + // defined in the type. + for (var x in type.lengths) { + var expected_lenght = type.lengths[x]; + if (number.length === expected_lenght) { + return true; + } + } + + return false; + }; + + /** + * Trigger all other validations. + * + * @param {object} element + */ + var cardInputBlur = function (element) { + var value = element.val(); + // Strip spaces from the value for all validations. + value = value.replace(/ /gi, ''); + + // If the value is not filled in, don't do any validation. + var empty_value = value.length === 0; + if (empty_value) { + element.addClass('invalid-cc'); + return; + } + + // Get the type of the card. + var type = detectType(value); + + // If no type is found, don't bother doing anything else. + if (!type) { + element.addClass('invalid-cc'); + return; + } + element.addClass('credit_card'); + element.addClass('credit_card--' + type.type); + + var ValidationDiv = element.parent().parent().find('#cc-validation'); + ValidationDiv.html(''); + + var ccv_field = $("input[name='payment_information[add][payment_details][security_code]']"); + if (ccv_field.size() > 0) { + var ccv_value = ccv_field.val(); + if (ccv_value.length == 0) { + ValidationDiv.append('CCV is not filled in'); + return; + } + } + + ValidationDiv.append('CC is of type: ' + type.niceType); + + // Check if the card is actually valid as well. + var is_valid = validateCreditCard(value, type); + if (is_valid) { + ValidationDiv.append(' CC is valid'); + element.removeClass('invalid-cc'); + element.addClass('valid-cc'); + } + else { + ValidationDiv.append(' CC is not valid'); + element.removeClass('valid-cc'); + element.addClass('invalid-cc'); + } + }; + + /** + * Element onkey upvalidation. + * + * @param {object} element + */ + var cardInputKeyup = function (element) { + var value = element.val(); + // Strip spaces from the value for all validations. + value = value.replace(/ /gi, ''); + + // If the value is not filled in, don't do any validation. + var empty_value = value.length === 0; + if (empty_value) { + element.addClass('invalid-cc'); + return; + } + + // Get the type of the card. + var type = detectType(value); + + // If no type is found, don't bother doing anything else. + if (!type) { + element.addClass('invalid-cc'); + return; + } + element.removeClass('invalid-cc'); + element.addClass('credit_card'); + element.addClass('credit_card--' + type.type); + }; + + Drupal.behaviors.creditCardValidation = { + attach: function (context, settings) { + $('#edit-payment-information-add-payment-details-number', context).each(function () { + var element = $(this); + $(element).on('blur', function () { + cardInputBlur(element); + }); + $(element).on('keyup', function() { + cardInputKeyup(element); + }); + }); + } + }; + +})(jQuery); diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php index 44c6c69bb1..5b2e97f09d 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -47,6 +47,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta ]; if ($payment_method->bundle() == 'credit_card') { $form['payment_details'] = $this->buildCreditCardForm($form['payment_details'], $form_state); + $form['#attached']['library'][] = 'commerce_payment/credit_card_validation'; } elseif ($payment_method->bundle() == 'paypal') { $form['payment_details'] = $this->buildPayPalForm($form['payment_details'], $form_state); @@ -173,6 +174,7 @@ protected function buildCreditCardForm(array $element, FormStateInterface $form_ '#required' => TRUE, '#maxlength' => 19, '#size' => 20, + '#suffix' => '
', ]; $element['expiration'] = [ '#type' => 'container',