diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/payment/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/payment/static')
27 files changed, 1756 insertions, 0 deletions
diff --git a/addons/payment/static/description/icon.png b/addons/payment/static/description/icon.png Binary files differnew file mode 100644 index 00000000..bd9a4e06 --- /dev/null +++ b/addons/payment/static/description/icon.png diff --git a/addons/payment/static/description/icon.svg b/addons/payment/static/description/icon.svg new file mode 100644 index 00000000..cff0a33b --- /dev/null +++ b/addons/payment/static/description/icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="98.616%"><stop offset="0%" stop-color="#797C79"/><stop offset="100%" stop-color="#545554"/></linearGradient><path id="d" d="M19.25 34.621C20.983 35.541 22.9 36 25 36c4 0 7-1.667 9-5h15.036v-3.212a.342.342 0 0 0-.339-.343H35c.347-.502.423-1.416.228-2.742h13.808c1.5 0 2.714 1.228 2.714 2.742v20.11c0 1.514-1.213 2.742-2.714 2.742H21.964c-1.5 0-2.714-1.228-2.714-2.742V34.62zm29.447 12.934a.342.342 0 0 0 .339-.343V37.5H21.964v9.712c0 .188.152.343.339.343h26.394zm-18.614-5.713v2.285a.683.683 0 0 1-.677.685h-4.062a.683.683 0 0 1-.677-.685v-2.285c0-.377.304-.686.677-.686h4.062c.373 0 .677.309.677.686zm10.834 0v2.285a.683.683 0 0 1-.677.685h-7.674a.683.683 0 0 1-.677-.685v-2.285c0-.377.305-.686.677-.686h7.674c.372 0 .677.309.677.686zM22.874 19.77c0 2.517 8.146 1.879 8.146 6.663 0 2.292-1.783 4.25-4.681 4.657v1.897c0 .265-.237.48-.53.48h-1.763c-.292 0-.53-.215-.53-.48v-1.928c-1.74-.268-3.293-.997-4.353-1.917a.447.447 0 0 1-.058-.634l1.34-1.625a.566.566 0 0 1 .757-.086c1.098.8 2.517 1.435 3.873 1.435 1.58 0 2.299-.853 2.299-1.646 0-2.342-8.147-1.834-8.147-6.772 0-2.107 1.705-3.811 4.29-4.342V13.48c0-.265.237-.48.53-.48h1.763c.292 0 .529.215.529.48v1.888c1.418.149 2.954.653 4.025 1.511.184.148.23.391.113.587l-1.038 1.74c-.152.255-.516.33-.775.161-.985-.642-2.193-1.132-3.371-1.132-1.47 0-2.42.603-2.42 1.535z"/><path id="e" d="M19.25 32.621C20.983 33.541 22.9 34 25 34c4 0 7-1.667 9-5h15.036v-3.212a.342.342 0 0 0-.339-.343H35c.347-.502.423-1.416.228-2.742h13.808c1.5 0 2.714 1.228 2.714 2.742v20.11c0 1.514-1.213 2.742-2.714 2.742H21.964c-1.5 0-2.714-1.228-2.714-2.742V32.62zm29.447 12.934a.342.342 0 0 0 .339-.343V35.5H21.964v9.712c0 .188.152.343.339.343h26.394zm-18.614-5.713v2.285a.683.683 0 0 1-.677.685h-4.062a.683.683 0 0 1-.677-.685v-2.285c0-.377.304-.686.677-.686h4.062c.373 0 .677.309.677.686zm10.834 0v2.285a.683.683 0 0 1-.677.685h-7.674a.683.683 0 0 1-.677-.685v-2.285c0-.377.305-.686.677-.686h7.674c.372 0 .677.309.677.686zM22.874 17.77c0 2.517 8.146 1.879 8.146 6.663 0 2.292-1.783 4.25-4.681 4.657v1.897c0 .265-.237.48-.53.48h-1.763c-.292 0-.53-.215-.53-.48v-1.928c-1.74-.268-3.293-.997-4.353-1.917a.447.447 0 0 1-.058-.634l1.34-1.625a.566.566 0 0 1 .757-.086c1.098.8 2.517 1.435 3.873 1.435 1.58 0 2.299-.853 2.299-1.646 0-2.342-8.147-1.834-8.147-6.772 0-2.107 1.705-3.811 4.29-4.342V11.48c0-.265.237-.48.53-.48h1.763c.292 0 .529.215.529.48v1.888c1.418.149 2.954.653 4.025 1.511.184.148.23.391.113.587l-1.038 1.74c-.152.255-.516.33-.775.161-.985-.642-2.193-1.132-3.371-1.132-1.47 0-2.42.603-2.42 1.535z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M4 69c-2 0-4-1-4-4V36.095l23.6-24.837 5.552 6.125-3.147 3.581 4.02 6.176 5.257-4.413L48 23l3.377 2.017v21.868L32.142 69H4z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" xlink:href="#e"/></g></g></svg>
\ No newline at end of file diff --git a/addons/payment/static/img/american_express.png b/addons/payment/static/img/american_express.png Binary files differnew file mode 100644 index 00000000..9f31d21e --- /dev/null +++ b/addons/payment/static/img/american_express.png diff --git a/addons/payment/static/img/bancontact.png b/addons/payment/static/img/bancontact.png Binary files differnew file mode 100644 index 00000000..7941a69c --- /dev/null +++ b/addons/payment/static/img/bancontact.png diff --git a/addons/payment/static/img/cirrus.png b/addons/payment/static/img/cirrus.png Binary files differnew file mode 100644 index 00000000..eb4b2aa8 --- /dev/null +++ b/addons/payment/static/img/cirrus.png diff --git a/addons/payment/static/img/codensa_easy_credit.png b/addons/payment/static/img/codensa_easy_credit.png Binary files differnew file mode 100644 index 00000000..7f5d4875 --- /dev/null +++ b/addons/payment/static/img/codensa_easy_credit.png diff --git a/addons/payment/static/img/diners_club_intl.png b/addons/payment/static/img/diners_club_intl.png Binary files differnew file mode 100644 index 00000000..888860a1 --- /dev/null +++ b/addons/payment/static/img/diners_club_intl.png diff --git a/addons/payment/static/img/discover.png b/addons/payment/static/img/discover.png Binary files differnew file mode 100644 index 00000000..d9222265 --- /dev/null +++ b/addons/payment/static/img/discover.png diff --git a/addons/payment/static/img/eps.png b/addons/payment/static/img/eps.png Binary files differnew file mode 100644 index 00000000..71ca1305 --- /dev/null +++ b/addons/payment/static/img/eps.png diff --git a/addons/payment/static/img/giropay.png b/addons/payment/static/img/giropay.png Binary files differnew file mode 100644 index 00000000..8869385f --- /dev/null +++ b/addons/payment/static/img/giropay.png diff --git a/addons/payment/static/img/ideal.png b/addons/payment/static/img/ideal.png Binary files differnew file mode 100644 index 00000000..e506097c --- /dev/null +++ b/addons/payment/static/img/ideal.png diff --git a/addons/payment/static/img/jcb.png b/addons/payment/static/img/jcb.png Binary files differnew file mode 100644 index 00000000..f3e3c5bf --- /dev/null +++ b/addons/payment/static/img/jcb.png diff --git a/addons/payment/static/img/maestro.png b/addons/payment/static/img/maestro.png Binary files differnew file mode 100644 index 00000000..a0a6b328 --- /dev/null +++ b/addons/payment/static/img/maestro.png diff --git a/addons/payment/static/img/mastercard.png b/addons/payment/static/img/mastercard.png Binary files differnew file mode 100644 index 00000000..2572a33b --- /dev/null +++ b/addons/payment/static/img/mastercard.png diff --git a/addons/payment/static/img/p24.png b/addons/payment/static/img/p24.png Binary files differnew file mode 100644 index 00000000..5ed3f7bf --- /dev/null +++ b/addons/payment/static/img/p24.png diff --git a/addons/payment/static/img/unionpay.png b/addons/payment/static/img/unionpay.png Binary files differnew file mode 100644 index 00000000..8d830c5f --- /dev/null +++ b/addons/payment/static/img/unionpay.png diff --git a/addons/payment/static/img/visa.png b/addons/payment/static/img/visa.png Binary files differnew file mode 100644 index 00000000..58cfab08 --- /dev/null +++ b/addons/payment/static/img/visa.png diff --git a/addons/payment/static/img/webmoney.png b/addons/payment/static/img/webmoney.png Binary files differnew file mode 100644 index 00000000..2e4e5916 --- /dev/null +++ b/addons/payment/static/img/webmoney.png diff --git a/addons/payment/static/img/western_union.png b/addons/payment/static/img/western_union.png Binary files differnew file mode 100644 index 00000000..02969bd5 --- /dev/null +++ b/addons/payment/static/img/western_union.png diff --git a/addons/payment/static/lib/jquery.payment/jquery.payment.js b/addons/payment/static/lib/jquery.payment/jquery.payment.js new file mode 100644 index 00000000..dcf829fb --- /dev/null +++ b/addons/payment/static/lib/jquery.payment/jquery.payment.js @@ -0,0 +1,652 @@ +// Generated by CoffeeScript 1.7.1 +(function() { + var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlashAndSpace, hasTextSelected, luhnCheck, reFormatCVC, reFormatCardNumber, reFormatExpiry, reFormatNumeric, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, safeVal, 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; }; + + $ = window.jQuery || window.Zepto || window.$; + + $.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; + + $.payment.cards = cards = [ + { + type: 'maestro', + patterns: [5018, 502, 503, 506, 56, 58, 639, 6220, 67], + format: defaultFormat, + length: [12, 13, 14, 15, 16, 17, 18, 19], + cvcLength: [3], + luhn: true + }, { + type: 'forbrugsforeningen', + patterns: [600], + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'dankort', + patterns: [5019], + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'visa', + patterns: [4], + format: defaultFormat, + length: [13, 16], + cvcLength: [3], + luhn: true + }, { + type: 'mastercard', + patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27], + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'amex', + patterns: [34, 37], + format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, + length: [15], + cvcLength: [3, 4], + luhn: true + }, { + type: 'dinersclub', + patterns: [30, 36, 38, 39], + format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, + length: [14], + cvcLength: [3], + luhn: true + }, { + type: 'discover', + patterns: [60, 64, 65, 622], + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'unionpay', + patterns: [62, 88], + format: defaultFormat, + length: [16, 17, 18, 19], + cvcLength: [3], + luhn: false + }, { + type: 'jcb', + patterns: [35], + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + } + ]; + + cardFromNumber = function(num) { + var card, p, pattern, _i, _j, _len, _len1, _ref; + num = (num + '').replace(/\D/g, ''); + for (_i = 0, _len = cards.length; _i < _len; _i++) { + card = cards[_i]; + _ref = card.patterns; + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + pattern = _ref[_j]; + p = pattern + ''; + if (num.substr(0, p.length) === p) { + 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 ? _ref.createRange : void 0 : void 0) != null) { + if (document.selection.createRange().text) { + return true; + } + } + return false; + }; + + safeVal = function(value, $target) { + var currPair, cursor, digit, error, last, prevPair; + try { + cursor = $target.prop('selectionStart'); + } catch (_error) { + error = _error; + cursor = null; + } + last = $target.val(); + $target.val(value); + if (cursor !== null && $target.is(":focus")) { + if (cursor === last.length) { + cursor = value.length; + } + if (last !== value) { + prevPair = last.slice(cursor - 1, +cursor + 1 || 9e9); + currPair = value.slice(cursor - 1, +cursor + 1 || 9e9); + digit = value[cursor]; + if (/\d/.test(digit) && prevPair === ("" + digit + " ") && currPair === (" " + digit)) { + cursor = cursor + 1; + } + } + $target.prop('selectionStart', cursor); + return $target.prop('selectionEnd', cursor); + } + }; + + replaceFullWidthChars = function(str) { + var chars, chr, fullWidth, halfWidth, idx, value, _i, _len; + if (str == null) { + str = ''; + } + fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; + halfWidth = '0123456789'; + value = ''; + chars = str.split(''); + for (_i = 0, _len = chars.length; _i < _len; _i++) { + chr = chars[_i]; + idx = fullWidth.indexOf(chr); + if (idx > -1) { + chr = halfWidth[idx]; + } + value += chr; + } + return value; + }; + + reFormatNumeric = function(e) { + var $target; + $target = $(e.currentTarget); + return setTimeout(function() { + var value; + value = $target.val(); + value = replaceFullWidthChars(value); + value = value.replace(/\D/g, ''); + return safeVal(value, $target); + }); + }; + + reFormatCardNumber = function(e) { + var $target; + $target = $(e.currentTarget); + return setTimeout(function() { + var value; + value = $target.val(); + value = replaceFullWidthChars(value); + value = $.payment.formatCardNumber(value); + return safeVal(value, $target); + }); + }; + + 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 setTimeout(function() { + return $target.val(value + ' ' + digit); + }); + } else if (re.test(value + digit)) { + e.preventDefault(); + return setTimeout(function() { + return $target.val(value + digit + ' '); + }); + } + }; + + formatBackCardNumber = function(e) { + var $target, value; + $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 setTimeout(function() { + return $target.val(value.replace(/\d\s$/, '')); + }); + } else if (/\s\d?$/.test(value)) { + e.preventDefault(); + return setTimeout(function() { + return $target.val(value.replace(/\d$/, '')); + }); + } + }; + + reFormatExpiry = function(e) { + var $target; + $target = $(e.currentTarget); + return setTimeout(function() { + var value; + value = $target.val(); + value = replaceFullWidthChars(value); + value = $.payment.formatExpiry(value); + return safeVal(value, $target); + }); + }; + + 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 setTimeout(function() { + return $target.val("0" + val + " / "); + }); + } else if (/^\d\d$/.test(val)) { + e.preventDefault(); + return setTimeout(function() { + var m1, m2; + m1 = parseInt(val[0], 10); + m2 = parseInt(val[1], 10); + if (m2 > 2 && m1 !== 0) { + return $target.val("0" + m1 + " / " + m2); + } else { + 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 + " / "); + } + }; + + formatForwardSlashAndSpace = function(e) { + var $target, val, which; + which = String.fromCharCode(e.which); + if (!(which === '/' || which === ' ')) { + return; + } + $target = $(e.currentTarget); + val = $target.val(); + if (/^\d$/.test(val) && val !== '0') { + return $target.val("0" + val + " / "); + } + }; + + formatBackExpiry = function(e) { + var $target, value; + $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\/\s$/.test(value)) { + e.preventDefault(); + return setTimeout(function() { + return $target.val(value.replace(/\d\s\/\s$/, '')); + }); + } + }; + + reFormatCVC = function(e) { + var $target; + $target = $(e.currentTarget); + return setTimeout(function() { + var value; + value = $target.val(); + value = replaceFullWidthChars(value); + value = value.replace(/\D/g, '').slice(0, 4); + return safeVal(value, $target); + }); + }; + + 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; + } + if (hasTextSelected($target)) { + 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.on('keypress', restrictNumeric); + this.on('keypress', restrictCVC); + this.on('paste', reFormatCVC); + this.on('change', reFormatCVC); + this.on('input', reFormatCVC); + return this; + }; + + $.payment.fn.formatCardExpiry = function() { + this.on('keypress', restrictNumeric); + this.on('keypress', restrictExpiry); + this.on('keypress', formatExpiry); + this.on('keypress', formatForwardSlashAndSpace); + this.on('keypress', formatForwardExpiry); + this.on('keydown', formatBackExpiry); + this.on('change', reFormatExpiry); + this.on('input', reFormatExpiry); + return this; + }; + + $.payment.fn.formatCardNumber = function() { + this.on('keypress', restrictNumeric); + this.on('keypress', restrictCardNumber); + this.on('keypress', formatCardNumber); + this.on('keydown', formatBackCardNumber); + this.on('keyup', setCardType); + this.on('paste', reFormatCardNumber); + this.on('change', reFormatCardNumber); + this.on('input', reFormatCardNumber); + this.on('input', setCardType); + return this; + }; + + $.payment.fn.restrictNumeric = function() { + this.on('keypress', restrictNumeric); + this.on('paste', reFormatNumeric); + this.on('change', reFormatNumeric); + this.on('input', reFormatNumeric); + return this; + }; + + $.payment.fn.cardExpiryVal = function() { + return $.payment.cardExpiryVal($(this).val()); + }; + + $.payment.cardExpiryVal = function(value) { + var month, prefix, year, _ref; + _ref = value.split(/[\s\/]+/, 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, _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 (!((1 <= month && month <= 12))) { + return false; + } + if (year.length === 2) { + if (year < 70) { + year = "20" + year; + } else { + year = "19" + year; + } + } + if (year.length !== 4) { + return false; + } + 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 card, _ref; + cvc = $.trim(cvc); + if (!/^\d+$/.test(cvc)) { + return false; + } + card = cardFromType(type); + if (card != null) { + return _ref = cvc.length, __indexOf.call(card.cvcLength, _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; + num = num.replace(/\D/g, ''); + card = cardFromNumber(num); + if (!card) { + return num; + } + upperLength = card.length[card.length.length - 1]; + num = num.slice(0, upperLength); + if (card.format.global) { + return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0; + } else { + groups = card.format.exec(num); + if (groups == null) { + return; + } + groups.shift(); + groups = $.grep(groups, function(n) { + return n; + }); + return groups.join(' '); + } + }; + + $.payment.formatExpiry = function(expiry) { + var mon, parts, sep, year; + parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); + if (!parts) { + return ''; + } + mon = parts[1] || ''; + sep = parts[2] || ''; + year = parts[3] || ''; + if (year.length > 0) { + sep = ' / '; + } else if (sep === ' /') { + mon = mon.substring(0, 1); + sep = ''; + } else if (mon.length === 2 || sep.length > 0) { + sep = ' / '; + } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) { + mon = "0" + mon; + sep = ' / '; + } + return mon + sep + year; + }; + +}).call(this); diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js new file mode 100644 index 00000000..e74dfe3e --- /dev/null +++ b/addons/payment/static/src/js/payment_form.js @@ -0,0 +1,588 @@ +odoo.define('payment.payment_form', function (require) { +"use strict"; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var publicWidget = require('web.public.widget'); + +var _t = core._t; + +publicWidget.registry.PaymentForm = publicWidget.Widget.extend({ + selector: '.o_payment_form', + events: { + 'submit': 'onSubmit', + 'click #o_payment_form_pay': 'payEvent', + 'click #o_payment_form_add_pm': 'addPmEvent', + 'click button[name="delete_pm"]': 'deletePmEvent', + 'click .o_payment_form_pay_icon_more': 'onClickMorePaymentIcon', + 'click .o_payment_acquirer_select': 'radioClickEvent', + }, + + /** + * @override + */ + start: function () { + this._adaptPayButton(); + window.addEventListener('pageshow', function (event) { + if (event.persisted) { + window.location.reload(); + } + }); + var self = this; + return this._super.apply(this, arguments).then(function () { + self.options = _.extend(self.$el.data(), self.options); + self.updateNewPaymentDisplayStatus(); + $('[data-toggle="tooltip"]').tooltip(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} title + * @param {string} message + */ + displayError: function (title, message) { + var $checkedRadio = this.$('input[type="radio"]:checked'), + acquirerID = this.getAcquirerIdFromRadio($checkedRadio[0]); + var $acquirerForm; + if (this.isNewPaymentRadio($checkedRadio[0])) { + $acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID); + } + else if (this.isFormPaymentRadio($checkedRadio[0])) { + $acquirerForm = this.$('#o_payment_form_acq_' + acquirerID); + } + + if ($checkedRadio.length === 0) { + return new Dialog(null, { + title: _t('Error: ') + _.str.escapeHTML(title), + size: 'medium', + $content: "<p>" + (_.str.escapeHTML(message) || "") + "</p>" , + buttons: [ + {text: _t('Ok'), close: true}]}).open(); + } else { + // removed if exist error message + this.$('#payment_error').remove(); + var messageResult = '<div class="alert alert-danger mb4" id="payment_error">'; + if (title != '') { + messageResult = messageResult + '<b>' + _.str.escapeHTML(title) + ':</b><br/>'; + } + messageResult = messageResult + _.str.escapeHTML(message) + '</div>'; + $acquirerForm.append(messageResult); + } + }, + hideError: function() { + this.$('#payment_error').remove(); + }, + /** + * @private + * @param {DOMElement} element + */ + getAcquirerIdFromRadio: function (element) { + return $(element).data('acquirer-id'); + }, + /** + * @private + * @param {jQuery} $form + */ + getFormData: function ($form) { + var unindexed_array = $form.serializeArray(); + var indexed_array = {}; + + $.map(unindexed_array, function (n, i) { + indexed_array[n.name] = n.value; + }); + return indexed_array; + }, + /** + * @private + * @param {DOMElement} element + */ + isFormPaymentRadio: function (element) { + return $(element).data('form-payment') === 'True'; + }, + /** + * @private + * @param {DOMElement} element + */ + isNewPaymentRadio: function (element) { + return $(element).data('s2s-payment') === 'True'; + }, + /** + * @private + */ + updateNewPaymentDisplayStatus: function () { + var checked_radio = this.$('input[type="radio"]:checked'); + // we hide all the acquirers form + this.$('[id*="o_payment_add_token_acq_"]').addClass('d-none'); + this.$('[id*="o_payment_form_acq_"]').addClass('d-none'); + if (checked_radio.length !== 1) { + return; + } + checked_radio = checked_radio[0]; + var acquirer_id = this.getAcquirerIdFromRadio(checked_radio); + + // if we clicked on an add new payment radio, display its form + if (this.isNewPaymentRadio(checked_radio)) { + this.$('#o_payment_add_token_acq_' + acquirer_id).removeClass('d-none'); + } + else if (this.isFormPaymentRadio(checked_radio)) { + this.$('#o_payment_form_acq_' + acquirer_id).removeClass('d-none'); + } + }, + + disableButton: function (button) { + $("body").block({overlayCSS: {backgroundColor: "#000", opacity: 0, zIndex: 1050}, message: false}); + $(button).attr('disabled', true); + $(button).children('.fa-lock').removeClass('fa-lock'); + $(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i> </span>'); + }, + + enableButton: function (button) { + $('body').unblock(); + $(button).attr('disabled', false); + $(button).children('.fa').addClass('fa-lock'); + $(button).find('span.o_loader').remove(); + }, + _parseError: function(e) { + if (e.message.data.arguments[1]) { + return e.message.data.arguments[0] + e.message.data.arguments[1]; + } + return e.message.data.arguments[0]; + }, + _adaptPayButton: function () { + var $payButton = $("#o_payment_form_pay"); + var disabledReasons = $payButton.data('disabled_reasons') || {}; + $payButton.prop('disabled', _.contains(disabledReasons, true)); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + payEvent: function (ev) { + ev.preventDefault(); + var form = this.el; + var checked_radio = this.$('input[type="radio"]:checked'); + var self = this; + if (ev.type === 'submit') { + var button = $(ev.target).find('*[type="submit"]')[0] + } else { + var button = ev.target; + } + + // first we check that the user has selected a payment method + if (checked_radio.length === 1) { + checked_radio = checked_radio[0]; + + // we retrieve all the input inside the acquirer form and 'serialize' them to an indexed array + var acquirer_id = this.getAcquirerIdFromRadio(checked_radio); + var acquirer_form = false; + if (this.isNewPaymentRadio(checked_radio)) { + acquirer_form = this.$('#o_payment_add_token_acq_' + acquirer_id); + } else { + acquirer_form = this.$('#o_payment_form_acq_' + acquirer_id); + } + var inputs_form = $('input', acquirer_form); + var ds = $('input[name="data_set"]', acquirer_form)[0]; + + // if the user is adding a new payment + if (this.isNewPaymentRadio(checked_radio)) { + if (this.options.partnerId === undefined) { + console.warn('payment_form: unset partner_id when adding new token; things could go wrong'); + } + var form_data = this.getFormData(inputs_form); + var wrong_input = false; + + inputs_form.toArray().forEach(function (element) { + //skip the check of non visible inputs + if ($(element).attr('type') == 'hidden') { + return true; + } + $(element).closest('div.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + $(element).siblings( ".o_invalid_field" ).remove(); + //force check of forms validity (useful for Firefox that refill forms automatically on f5) + $(element).trigger("focusout"); + if (element.dataset.isRequired && element.value.length === 0) { + $(element).closest('div.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + $(element).closest('div.form-group').append('<div style="color: red" class="o_invalid_field" aria-invalid="true">' + _.str.escapeHTML("The value is invalid.") + '</div>'); + wrong_input = true; + } + else if ($(element).closest('div.form-group').hasClass('o_has_error')) { + wrong_input = true; + $(element).closest('div.form-group').append('<div style="color: red" class="o_invalid_field" aria-invalid="true">' + _.str.escapeHTML("The value is invalid.") + '</div>'); + } + }); + + if (wrong_input) { + return; + } + + this.disableButton(button); + // do the call to the route stored in the 'data_set' input of the acquirer form, the data must be called 'create-route' + return this._rpc({ + route: ds.dataset.createRoute, + params: form_data, + }).then(function (data) { + // if the server has returned true + if (data.result) { + // and it need a 3DS authentication + if (data['3d_secure'] !== false) { + // then we display the 3DS page to the user + $("body").html(data['3d_secure']); + } + else { + checked_radio.value = data.id; // set the radio value to the new card id + form.submit(); + return new Promise(function () {}); + } + } + // if the server has returned false, we display an error + else { + if (data.error) { + self.displayError( + '', + data.error); + } else { // if the server doesn't provide an error message + self.displayError( + _t('Server Error'), + _t('e.g. Your credit card details are wrong. Please verify.')); + } + } + // here we remove the 'processing' icon from the 'add a new payment' button + self.enableButton(button); + }).guardedCatch(function (error) { + error.event.preventDefault(); + // if the rpc fails, pretty obvious + self.enableButton(button); + + self.displayError( + _t('Server Error'), + _t("We are not able to add your payment method at the moment.") + + self._parseError(error) + ); + }); + } + // if the user is going to pay with a form payment, then + else if (this.isFormPaymentRadio(checked_radio)) { + this.disableButton(button); + var $tx_url = this.$el.find('input[name="prepare_tx_url"]'); + // if there's a prepare tx url set + if ($tx_url.length === 1) { + // if the user wants to save his credit card info + var form_save_token = acquirer_form.find('input[name="o_payment_form_save_token"]').prop('checked'); + // then we call the route to prepare the transaction + return this._rpc({ + route: $tx_url[0].value, + params: { + 'acquirer_id': parseInt(acquirer_id), + 'save_token': form_save_token, + 'access_token': self.options.accessToken, + 'success_url': self.options.successUrl, + 'error_url': self.options.errorUrl, + 'callback_method': self.options.callbackMethod, + 'order_id': self.options.orderId, + 'invoice_id': self.options.invoiceId, + }, + }).then(function (result) { + if (result) { + // if the server sent us the html form, we create a form element + var newForm = document.createElement('form'); + newForm.setAttribute("method", self._get_redirect_form_method()); + newForm.setAttribute("provider", checked_radio.dataset.provider); + newForm.hidden = true; // hide it + newForm.innerHTML = result; // put the html sent by the server inside the form + var action_url = $(newForm).find('input[name="data_set"]').data('actionUrl'); + newForm.setAttribute("action", action_url); // set the action url + $(document.getElementsByTagName('body')[0]).append(newForm); // append the form to the body + $(newForm).find('input[data-remove-me]').remove(); // remove all the input that should be removed + if(action_url) { + newForm.submit(); // and finally submit the form + return new Promise(function () {}); + } + } + else { + self.displayError( + _t('Server Error'), + _t("We are not able to redirect you to the payment form.") + ); + self.enableButton(button); + } + }).guardedCatch(function (error) { + error.event.preventDefault(); + self.displayError( + _t('Server Error'), + _t("We are not able to redirect you to the payment form.") + " " + + self._parseError(error) + ); + self.enableButton(button); + }); + } + else { + // we append the form to the body and send it. + this.displayError( + _t("Cannot setup the payment"), + _t("We're unable to process your payment.") + ); + self.enableButton(button); + } + } + else { // if the user is using an old payment then we just submit the form + this.disableButton(button); + form.submit(); + return new Promise(function () {}); + } + } + else { + this.displayError( + _t('No payment method selected'), + _t('Please select a payment method.') + ); + this.enableButton(button); + } + }, + /** + * Return the HTTP method to be used by the redirect form. + * + * @private + * @return {string} The HTTP method, "post" by default + */ + _get_redirect_form_method: function () { + return "post"; + }, + /** + * Called when clicking on the button to add a new payment method. + * + * @private + * @param {Event} ev + */ + addPmEvent: function (ev) { + ev.stopPropagation(); + ev.preventDefault(); + var checked_radio = this.$('input[type="radio"]:checked'); + var self = this; + if (ev.type === 'submit') { + var button = $(ev.target).find('*[type="submit"]')[0] + } else { + var button = ev.target; + } + + // we check if the user has selected a 'add a new payment' option + if (checked_radio.length === 1 && this.isNewPaymentRadio(checked_radio[0])) { + // we retrieve which acquirer is used + checked_radio = checked_radio[0]; + var acquirer_id = this.getAcquirerIdFromRadio(checked_radio); + var acquirer_form = this.$('#o_payment_add_token_acq_' + acquirer_id); + // we retrieve all the input inside the acquirer form and 'serialize' them to an indexed array + var inputs_form = $('input', acquirer_form); + var form_data = this.getFormData(inputs_form); + var ds = $('input[name="data_set"]', acquirer_form)[0]; + var wrong_input = false; + + inputs_form.toArray().forEach(function (element) { + //skip the check of non visible inputs + if ($(element).attr('type') == 'hidden') { + return true; + } + $(element).closest('div.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + $(element).siblings( ".o_invalid_field" ).remove(); + //force check of forms validity (useful for Firefox that refill forms automatically on f5) + $(element).trigger("focusout"); + if (element.dataset.isRequired && element.value.length === 0) { + $(element).closest('div.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + var message = '<div style="color: red" class="o_invalid_field" aria-invalid="true">' + _.str.escapeHTML("The value is invalid.") + '</div>'; + $(element).closest('div.form-group').append(message); + wrong_input = true; + } + else if ($(element).closest('div.form-group').hasClass('o_has_error')) { + wrong_input = true; + var message = '<div style="color: red" class="o_invalid_field" aria-invalid="true">' + _.str.escapeHTML("The value is invalid.") + '</div>'; + $(element).closest('div.form-group').append(message); + } + }); + + if (wrong_input) { + return; + } + // We add a 'processing' icon into the 'add a new payment' button + $(button).attr('disabled', true); + $(button).children('.fa-plus-circle').removeClass('fa-plus-circle'); + $(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i> </span>'); + + // do the call to the route stored in the 'data_set' input of the acquirer form, the data must be called 'create-route' + this._rpc({ + route: ds.dataset.createRoute, + params: form_data, + }).then(function (data) { + // if the server has returned true + if (data.result) { + // and it need a 3DS authentication + if (data['3d_secure'] !== false) { + // then we display the 3DS page to the user + $("body").html(data['3d_secure']); + } + // if it doesn't require 3DS + else { + // we just go to the return_url or reload the page + if (form_data.return_url) { + window.location = form_data.return_url; + } + else { + window.location.reload(); + } + } + } + // if the server has returned false, we display an error + else { + if (data.error) { + self.displayError( + '', + data.error); + } else { // if the server doesn't provide an error message + self.displayError( + _t('Server Error'), + _t('e.g. Your credit card details are wrong. Please verify.')); + } + } + // here we remove the 'processing' icon from the 'add a new payment' button + $(button).attr('disabled', false); + $(button).children('.fa').addClass('fa-plus-circle'); + $(button).find('span.o_loader').remove(); + }).guardedCatch(function (error) { + error.event.preventDefault(); + // if the rpc fails, pretty obvious + $(button).attr('disabled', false); + $(button).children('.fa').addClass('fa-plus-circle'); + $(button).find('span.o_loader').remove(); + + self.displayError( + _t('Server error'), + _t("We are not able to add your payment method at the moment.") + + self._parseError(error) + ); + }); + } + else { + this.displayError( + _t('No payment method selected'), + _t('Please select the option to add a new payment method.') + ); + } + }, + /** + * Called when submitting the form (e.g. through the Return key). + * We need to check whether we are paying or adding a new pm and dispatch + * to the correct method. + * + * @private + * @param {Event} ev + */ + onSubmit: function(ev) { + ev.stopPropagation(); + ev.preventDefault(); + var button = $(ev.target).find('*[type="submit"]')[0] + if (button.id === 'o_payment_form_pay') { + return this.payEvent(ev); + } else if (button.id === 'o_payment_form_add_pm') { + return this.addPmEvent(ev); + } + return; + }, + /** + * Called when clicking on a button to delete a payment method. + * + * @private + * @param {Event} ev + */ + deletePmEvent: function (ev) { + ev.stopPropagation(); + ev.preventDefault(); + var self = this; + var pm_id = parseInt(ev.target.value); + + var tokenDelete = function () { + self._rpc({ + model: 'payment.token', + method: 'unlink', + args: [pm_id], + }).then(function (result) { + if (result === true) { + ev.target.closest('div').remove(); + } + }, function () { + self.displayError( + _t('Server Error'), + _t("We are not able to delete your payment method at the moment.") + ); + }); + }; + + this._rpc({ + model: 'payment.token', + method: 'get_linked_records', + args: [pm_id], + }).then(function (result) { + if (result[pm_id].length > 0) { + // if there's records linked to this payment method + var content = ''; + result[pm_id].forEach(function (sub) { + content += '<p><a href="' + sub.url + '" title="' + sub.description + '">' + sub.name + '</a></p>'; + }); + + content = $('<div>').html('<p>' + _t('This card is currently linked to the following records:') + '</p>' + content); + // Then we display the list of the records and ask the user if he really want to remove the payment method. + new Dialog(self, { + title: _t('Warning!'), + size: 'medium', + $content: content, + buttons: [ + {text: _t('Confirm Deletion'), classes: 'btn-primary', close: true, click: tokenDelete}, + {text: _t('Cancel'), close: true}] + }).open(); + } + else { + // if there's no records linked to this payment method, then we delete it + tokenDelete(); + } + }, function (err, event) { + self.displayError( + _t('Server Error'), + _t("We are not able to delete your payment method at the moment.") + err.data.message + ); + }); + }, + /** + * Called when clicking on 'and more' to show more payment icon. + * + * @private + * @param {Event} ev + */ + onClickMorePaymentIcon: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var $listItems = $(ev.currentTarget).parents('ul').children('li'); + var $moreItem = $(ev.currentTarget).parents('li'); + $listItems.removeClass('d-none'); + $moreItem.addClass('d-none'); + }, + /** + * Called when clicking on a radio button. + * + * @private + * @param {Event} ev + */ + radioClickEvent: function (ev) { + // radio button checked when we click on entire zone(body) of the payment acquirer + $(ev.currentTarget).find('input[type="radio"]').prop("checked", true); + this.updateNewPaymentDisplayStatus(); + }, +}); +return publicWidget.registry.PaymentForm; +}); diff --git a/addons/payment/static/src/js/payment_portal.js b/addons/payment/static/src/js/payment_portal.js new file mode 100644 index 00000000..82f8647f --- /dev/null +++ b/addons/payment/static/src/js/payment_portal.js @@ -0,0 +1,65 @@ +$(function () { + + $('input#cc_number').payment('formatCardNumber'); + $('input#cc_cvc').payment('formatCardCVC'); + $('input#cc_expiry').payment('formatCardExpiry') + + $('input#cc_number').on('focusout', function (e) { + var valid_value = $.payment.validateCardNumber(this.value); + var card_type = $.payment.cardType(this.value); + if (card_type) { + $(this).parent('.form-group').children('.card_placeholder').removeClass().addClass('card_placeholder ' + card_type); + $(this).parent('.form-group').children('input[name="cc_brand"]').val(card_type) + } + else { + $(this).parent('.form-group').children('.card_placeholder').removeClass().addClass('card_placeholder'); + } + if (valid_value) { + $(this).parent('.form-group').addClass('o_has_success').find('.form-control, .custom-select').addClass('is-valid'); + $(this).parent('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + $(this).siblings('.o_invalid_field').remove(); + } + else { + $(this).parent('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + $(this).parent('.form-group').removeClass('o_has_success').find('.form-control, .custom-select').removeClass('is-valid'); + } + }); + + $('input#cc_cvc').on('focusout', function (e) { + var cc_nbr = $(this).parents('.oe_cc').find('#cc_number').val(); + var card_type = $.payment.cardType(cc_nbr); + var valid_value = $.payment.validateCardCVC(this.value, card_type); + if (valid_value) { + $(this).parent('.form-group').addClass('o_has_success').find('.form-control, .custom-select').addClass('is-valid'); + $(this).parent('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + $(this).siblings('.o_invalid_field').remove(); + } + else { + $(this).parent('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + $(this).parent('.form-group').removeClass('o_has_success').find('.form-control, .custom-select').removeClass('is-valid'); + } + }); + + $('input#cc_expiry').on('focusout', function (e) { + var expiry_value = $.payment.cardExpiryVal(this.value); + var month = expiry_value.month || ''; + var year = expiry_value.year || ''; + var valid_value = $.payment.validateCardExpiry(month, year); + if (valid_value) { + $(this).parent('.form-group').addClass('o_has_success').find('.form-control, .custom-select').addClass('is-valid'); + $(this).parent('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + $(this).siblings('.o_invalid_field').remove(); + } + else { + $(this).parent('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + $(this).parent('.form-group').removeClass('o_has_success').find('.form-control, .custom-select').removeClass('is-valid'); + } + }); + + $('select[name="pm_acquirer_id"]').on('change', function() { + var acquirer_id = $(this).val(); + $('.acquirer').addClass('d-none'); + $('.acquirer[data-acquirer-id="'+acquirer_id+'"]').removeClass('d-none'); + }); + +}); diff --git a/addons/payment/static/src/js/payment_processing.js b/addons/payment/static/src/js/payment_processing.js new file mode 100644 index 00000000..5ed13cf1 --- /dev/null +++ b/addons/payment/static/src/js/payment_processing.js @@ -0,0 +1,121 @@ +odoo.define('payment.processing', function (require) { + 'use strict'; + + var publicWidget = require('web.public.widget'); + var ajax = require('web.ajax'); + var core = require('web.core'); + + var _t = core._t; + + $.blockUI.defaults.css.border = '0'; + $.blockUI.defaults.css["background-color"] = ''; + $.blockUI.defaults.overlayCSS["opacity"] = '0.9'; + + publicWidget.registry.PaymentProcessing = publicWidget.Widget.extend({ + selector: '.o_payment_processing', + xmlDependencies: ['/payment/static/src/xml/payment_processing.xml'], + + _pollCount: 0, + + start: function() { + this.displayLoading(); + this.poll(); + return this._super.apply(this, arguments); + }, + /* Methods */ + startPolling: function () { + var timeout = 3000; + // + if(this._pollCount >= 10 && this._pollCount < 20) { + timeout = 10000; + } + else if(this._pollCount >= 20) { + timeout = 30000; + } + // + setTimeout(this.poll.bind(this), timeout); + this._pollCount ++; + }, + poll: function () { + var self = this; + ajax.jsonRpc('/payment/process/poll', 'call', {}).then(function(data) { + if(data.success === true) { + self.processPolledData(data.transactions); + } + else { + switch(data.error) { + case "tx_process_retry": + break; + case "no_tx_found": + self.displayContent("payment.no_tx_found", {}); + break; + default: // if an exception is raised + self.displayContent("payment.exception", {exception_msg: data.error}); + break; + } + } + self.startPolling(); + + }).guardedCatch(function() { + self.displayContent("payment.rpc_error", {}); + self.startPolling(); + }); + }, + processPolledData: function (transactions) { + var render_values = { + 'tx_draft': [], + 'tx_pending': [], + 'tx_authorized': [], + 'tx_done': [], + 'tx_cancel': [], + 'tx_error': [], + }; + + if (transactions.length > 0 && ['transfer', 'sepa_direct_debit'].indexOf(transactions[0].acquirer_provider) >= 0) { + window.location = transactions[0].return_url; + return; + } + + // group the transaction according to their state + transactions.forEach(function (tx) { + var key = 'tx_' + tx.state; + if(key in render_values) { + render_values[key].push(tx); + } + }); + + function countTxInState(states) { + var nbTx = 0; + for (var prop in render_values) { + if (states.indexOf(prop) > -1 && render_values.hasOwnProperty(prop)) { + nbTx += render_values[prop].length; + } + } + return nbTx; + } + // if there's only one tx to manage + if(countTxInState(['tx_done', 'tx_error', 'tx_pending', 'tx_authorized']) === 1) { + var tx = render_values['tx_done'][0] || render_values['tx_authorized'][0] || render_values['tx_error'][0]; + if (tx) { + window.location = tx.return_url; + return; + } + } + + this.displayContent("payment.display_tx_list", render_values); + }, + displayContent: function (xmlid, render_values) { + var html = core.qweb.render(xmlid, render_values); + $.unblockUI(); + this.$el.find('.o_payment_processing_content').html(html); + }, + displayLoading: function () { + var msg = _t("We are processing your payment, please wait ..."); + $.blockUI({ + 'message': '<h2 class="text-white"><img src="/web/static/src/img/spin.png" class="fa-pulse"/>' + + ' <br />' + msg + + '</h2>' + }); + }, + }); +}); diff --git a/addons/payment/static/src/scss/payment_acquirer.scss b/addons/payment/static/src/scss/payment_acquirer.scss new file mode 100644 index 00000000..e73582ec --- /dev/null +++ b/addons/payment/static/src/scss/payment_acquirer.scss @@ -0,0 +1,62 @@ +.o_kanban_view.o_kanban_payment_acquirer { + &.o_kanban_ungrouped { + .o_kanban_record { + width: 500px; + + .o_kanban_image { + float: right; + + + div { + padding-left: 0; + padding-right: $o-kanban-image-width + $o-kanban-inside-hgutter; + } + } + + .o_payment_acquirer_desc { + margin-bottom: 28px; + + i.fa { + margin-right: 5px; + + &.fa-check { + color: green; + } + } + } + + .o_payment_acquirer_bottom { + > button { + position: absolute; + bottom: 8px; + right: 8px; + } + > .label { + position: absolute; + bottom: 8px; + left: 8px; + } + } + } + } +} + +.o_form_view { + .o_payment_acquirer_desc { + margin-top: 10px; + ul { + list-style-type: none; + padding: 0; + + i.fa { + margin-right: 5px; + + &.fa-check { + color: green; + } + } + } + } + .o_warning_text { + color: #f0ad4e; + } +} diff --git a/addons/payment/static/src/scss/payment_form.scss b/addons/payment/static/src/scss/payment_form.scss new file mode 100644 index 00000000..de1e0102 --- /dev/null +++ b/addons/payment/static/src/scss/payment_form.scss @@ -0,0 +1,56 @@ +.o_payment_form { + label > input[type="radio"], input[type="checkbox"]{ + vertical-align: middle; + margin-right: 5px; + } + + .payment_option_name { + font-size: 14px; + font-weight: normal !important; + font-family: Helvetica Neue, sans-serif; + line-height: 1.3em; + color: #4d4d4d; + } + + label { + font-weight: normal; + margin-top: 5px; + } + + .card-body:first-child { + border-top: 0px; + } + + .card { + border-radius: 10px; + } + + .card-footer:last-child { + border-bottom-right-radius: 10px !important; + border-bottom-left-radius: 10px !important; + } + + .card-body { + border-top: 1px solid #ddd; + padding: 1.14em !important; + &.o_payment_acquirer_select:hover { + cursor: pointer; + } + } + + .payment_icon_list { + position: relative; + li { + padding-left: 5px !important; + padding-right: 0px !important; + } + + .more_option { + @include o-position-absolute($right: 10px); + font-size:10px; + } + + margin-top: 0px !important; + margin-bottom: -5px !important; + } +} diff --git a/addons/payment/static/src/scss/portal_payment.scss b/addons/payment/static/src/scss/portal_payment.scss new file mode 100644 index 00000000..4a28f596 --- /dev/null +++ b/addons/payment/static/src/scss/portal_payment.scss @@ -0,0 +1,61 @@ +input#cc_number { + background-repeat: no-repeat; + background-position: center right calc(2.7em); +} + +div.card_placeholder { + background-image: url("/website_payment/static/src/img/placeholder.png"); + background-repeat: no-repeat; + width: 32px; + height: 20px; + position: absolute; + top: 8px; + right: 20px; + -webkit-transition: 0.4s cubic-bezier(0.455,0.03,0.515,0.955); + transition: 0.4s cubic-bezier(0.455,0.03,0.515,0.955); + pointer-events: none; +} + +/* if s2s form not in bootstrap_formatting */ +div.o_card_brand_detail { + position: relative; + + div.card_placeholder { + right: 5px; + } +} + +div.amex { + background-image: url("/website_payment/static/src/img/amex.png"); + background-repeat: no-repeat; +} + +div.diners { + background-image: url("/website_payment/static/src/img/diners.png"); + background-repeat: no-repeat; +} + +div.discover { + background-image: url("/website_payment/static/src/img/discover.png"); + background-repeat: no-repeat; +} + +div.jcb { + background-image: url("/website_payment/static/src/img/jcb.png"); + background-repeat: no-repeat; +} + +div.mastercard { + background-image: url("/website_payment/static/src/img/mastercard.png"); + background-repeat: no-repeat; +} + +div.visa { + background-image: url("/website_payment/static/src/img/visa.png"); + background-repeat: no-repeat; +} + +ul.payment_method_list img.rounded { + max-width: 100px; + max-height: 40px; +} diff --git a/addons/payment/static/src/xml/payment_processing.xml b/addons/payment/static/src/xml/payment_processing.xml new file mode 100644 index 00000000..86623047 --- /dev/null +++ b/addons/payment/static/src/xml/payment_processing.xml @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="payment" xml:space="preserve"> + <!-- The templates here as rendered by 'payment_processing.js', you can also take + a look at payment_templates.xml (xmlid: payment_process_page) for more infos--> + <t t-name="payment.display_tx_list"> + <div> + <!-- Error transactions --> + <div t-if="tx_error.length > 0"> + <h1>Payments failed</h1> + <ul class="list-group"> + <t t-foreach="tx_error" t-as="tx"> + <li class="list-group-item"> + <h4 class="list-group-item-heading mb5"> + <t t-esc="tx['reference']"/> + <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span> + </h4> + <small class="list-group-item-text"> + An error occured during the processing of this payment.<br/> + <strong>Reason:</strong> <t t-esc="tx['state_message']"/> + </small> + </li> + </t> + </ul> + </div> + <div t-if="tx_done.length > 0 || tx_authorized.length > 0 || tx_pending.length > 0"> + <h1>Payments received</h1> + <div class="list-group"> + <!-- Done transactions --> + <t t-foreach="tx_done" t-as="tx"> + <a t-att-href="tx['return_url']" class="list-group-item"> + <h4 class="list-group-item-heading mb5"> + <t t-esc="tx['reference']"/> + <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span> + </h4> + <small class="list-group-item-text"> + <t t-if="!tx['is_processed']"> + Your order is being processed, please wait ... <i class="fa fa-cog fa-spin"/> + </t> + <t t-else=""> + Your order has been processed.<br/> + Click here to be redirected to the confirmation page. + </t> + </small> + </a> + </t> + <!-- Pending transactions --> + <t t-foreach="tx_pending" t-as="tx"> + <a t-att-href="tx['return_url']" class="list-group-item"> + <h4 class="list-group-item-heading mb5"> + <t t-esc="tx['reference']"/> + <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span> + </h4> + <small class="list-group-item-text"> + <t t-if="tx['message_to_display']"> + <t t-raw="tx['message_to_display']"/> + </t> + <t t-else=""> + Your payment is in pending state.<br/> + You will be notified when the payment is fully confirmed.<br/> + You can click here to be redirected to the confirmation page. + </t> + </small> + </a> + </t> + <!-- Authorized transactions --> + <t t-foreach="tx_authorized" t-as="tx"> + <li class="list-group-item"> + <h4 class="list-group-item-heading mb5"> + <t t-esc="tx['reference']"/> + <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span> + </h4> + <small class="list-group-item-text"> + <t t-if="tx['message_to_display']"> + <t t-raw="tx['message_to_display']"/> + </t> + <t t-else=""> + Your payment has been received but need to be confirmed manually.<br/> + You will be notified when the payment is confirmed. + </t> + </small> + </li> + </t> + </div> + </div> + <!-- Draft transactions --> + <div t-if="tx_draft.length > 0"> + <h1>Waiting for payment</h1> + <ul class="list-group"> + <t t-foreach="tx_draft" t-as="tx"> + <li class="list-group-item"> + <h4 class="list-group-item-heading mb5"> + <t t-esc="tx['reference']"/> + <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span> + </h4> + <small class="list-group-item-text"> + <t t-if="tx['message_to_display']"> + <t t-raw="tx['message_to_display']"/> + </t> + <t t-else=""> + We are waiting for the payment acquirer to confirm the payment. + </t> + </small> + </li> + </t> + </ul> + </div> + <!-- Cancel transactions --> + <div t-if="tx_cancel.length > 0"> + <h1>Cancelled payments</h1> + <ul class="list-group"> + <t t-foreach="tx_cancel" t-as="tx"> + <li class="list-group-item"> + <h4 class="list-group-item-heading mb5"> + <t t-esc="tx['reference']"/> + <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span> + </h4> + <small class="list-group-item-text"> + This transaction has been cancelled.<br/> + No payment has been processed. + </small> + </li> + </t> + </ul> + </div> + </div> + </t> + + <t t-name="payment.no_tx_found"> + <div class="text-center"> + <p>We are not able to find your payment, but don't worry.</p> + <p>You should receive an email confirming your payment in a few minutes.</p> + <p>If the payment hasn't been confirmed you can contact us.</p> + </div> + </t> + + <t t-name="payment.rpc_error"> + <div class="text-center"> + <p><strong>Server error:</strong> Unable to contact the Odoo server.</p> + <p>Please wait ... <i class="fa fa-refresh fa-spin"></i></p> + </div> + </t> + + <t t-name="payment.exception"> + <div class="text-center"> + <h2>Internal server error</h2> + <pre><t t-esc="exception_msg"/></pre> + </div> + </t> + +</templates> |
