This commit is contained in:
Ernad Husremovic 2025-08-29 17:40:39 +02:00
parent 12c29a983b
commit 95fcc8bd63
189 changed files with 170858 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -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>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,102 @@
odoo.define('payment.checkout_form', require => {
'use strict';
const publicWidget = require('web.public.widget');
const paymentFormMixin = require('payment.payment_form_mixin');
publicWidget.registry.PaymentCheckoutForm = publicWidget.Widget.extend(paymentFormMixin, {
selector: 'form[name="o_payment_checkout"]',
events: Object.assign({}, publicWidget.Widget.prototype.events, {
'click div[name="o_payment_option_card"]': '_onClickPaymentOption',
'click a[name="o_payment_icon_more"]': '_onClickMorePaymentIcons',
'click a[name="o_payment_icon_less"]': '_onClickLessPaymentIcons',
'click button[name="o_payment_submit_button"]': '_onClickPay',
'submit': '_onSubmit',
}),
/**
* @constructor
*/
init: function () {
const preventDoubleClick = handlerMethod => {
return _.debounce(handlerMethod, 500, true);
};
this._super(...arguments);
// Prevent double-clicks and browser glitches on all inputs
this._onClickLessPaymentIcons = preventDoubleClick(this._onClickLessPaymentIcons);
this._onClickMorePaymentIcons = preventDoubleClick(this._onClickMorePaymentIcons);
this._onClickPay = preventDoubleClick(this._onClickPay);
this._onClickPaymentOption = preventDoubleClick(this._onClickPaymentOption);
this._onSubmit = preventDoubleClick(this._onSubmit);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Handle a direct payment, a payment with redirection, or a payment by token.
*
* Called when clicking on the 'Pay' button or when submitting the form.
*
* @private
* @param {Event} ev
* @return {undefined}
*/
_onClickPay: async function (ev) {
ev.stopPropagation();
ev.preventDefault();
// Check that the user has selected a payment option
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
if (!this._ensureRadioIsChecked($checkedRadios)) {
return;
}
const checkedRadio = $checkedRadios[0];
// Extract contextual values from the radio button
const provider = this._getProviderFromRadio(checkedRadio);
const paymentOptionId = this._getPaymentOptionIdFromRadio(checkedRadio);
const flow = this._getPaymentFlowFromRadio(checkedRadio);
// Update the tx context with the value of the "Save my payment details" checkbox
if (flow !== 'token') {
const $tokenizeCheckbox = this.$(
`#o_payment_provider_inline_form_${paymentOptionId}` // Only match provider radios
).find('input[name="o_payment_save_as_token"]');
this.txContext.tokenizationRequested = $tokenizeCheckbox.length === 1
&& $tokenizeCheckbox[0].checked;
} else {
this.txContext.tokenizationRequested = false;
}
// Make the payment
this._hideError(); // Don't keep the error displayed if the user is going through 3DS2
this._disableButton(true); // Disable until it is needed again
$('body').block({
message: false,
overlayCSS: {backgroundColor: "#000", opacity: 0, zIndex: 1050},
});
this._processPayment(provider, paymentOptionId, flow);
},
/**
* Delegate the handling of the payment request to `_onClickPay`.
*
* Called when submitting the form (e.g. through the Return key).
*
* @private
* @param {Event} ev
* @return {undefined}
*/
_onSubmit: function (ev) {
ev.stopPropagation();
ev.preventDefault();
this._onClickPay(ev);
},
});
return publicWidget.registry.PaymentCheckoutForm;
});

View file

@ -0,0 +1,104 @@
/** @odoo-module */
import core from 'web.core';
import publicWidget from 'web.public.widget';
publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({
selector: 'form[name="o_payment_express_checkout_form"]',
/**
* @override
*/
start: async function () {
await this._super(...arguments);
this.txContext = {};
Object.assign(this.txContext, this.$el.data());
this.txContext.shippingInfoRequired = !!this.txContext.shippingInfoRequired;
const expressCheckoutForms = this._getExpressCheckoutForms();
for (const expressCheckoutForm of expressCheckoutForms) {
await this._prepareExpressCheckoutForm(expressCheckoutForm.dataset);
}
// Monitor updates of the amount on eCommerce's cart pages.
core.bus.on('cart_amount_changed', this, this._updateAmount.bind(this));
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Return all express checkout forms found on the page.
*
* @private
* @return {NodeList} - All express checkout forms found on the page.
*/
_getExpressCheckoutForms() {
return document.querySelectorAll(
'form[name="o_payment_express_checkout_form"] div[name="o_express_checkout_container"]'
);
},
/**
* Prepare the provider-specific express checkout form based on the provided data.
*
* For a provider to manage an express checkout form, it must override this method.
*
* @private
* @param {Object} providerData - The provider-specific data.
* @return {Promise}
*/
async _prepareExpressCheckoutForm(providerData) {
return Promise.resolve();
},
/**
* Prepare the params to send to the transaction route.
*
* For a provider to overwrite generic params or to add provider-specific ones, it must override
* this method and return the extended transaction route params.
*
* @private
* @param {number} providerId - The id of the provider handling the transaction.
* @returns {object} - The transaction route params
*/
_prepareTransactionRouteParams(providerId) {
return {
'payment_option_id': parseInt(providerId),
'reference_prefix': this.txContext.referencePrefix &&
this.txContent.referencePrefix.toString(),
'currency_id': this.txContext.currencyId &&
parseInt(this.txContext.currencyId),
'partner_id': parseInt(this.txContext.partnerId),
'flow': 'direct',
'tokenization_requested': false,
'landing_route': this.txContext.landingRoute,
'access_token': this.txContext.accessToken,
'csrf_token': core.csrf_token,
};
},
/**
* Update the amount of the express checkout form.
*
* For a provider to manage an express form, it must override this method.
*
* @private
* @param {number} newAmount - The new amount.
* @param {number} newMinorAmount - The new minor amount.
* @return {undefined}
*/
_updateAmount(newAmount, newMinorAmount) {
this.txContext.amount = parseFloat(newAmount);
this.txContext.minorAmount = parseInt(newMinorAmount);
this._getExpressCheckoutForms().forEach(form => {
if (newAmount == 0) {
form.classList.add('d-none')}
else {
form.classList.remove('d-none')
}
})
},
});
export const paymentExpressCheckoutForm = publicWidget.registry.PaymentExpressCheckoutForm;

View file

@ -0,0 +1,253 @@
odoo.define('payment.manage_form', require => {
'use strict';
const core = require('web.core');
const publicWidget = require('web.public.widget');
const Dialog = require('web.Dialog');
const paymentFormMixin = require('payment.payment_form_mixin');
const _t = core._t;
publicWidget.registry.PaymentManageForm = publicWidget.Widget.extend(paymentFormMixin, {
selector: 'form[name="o_payment_manage"]',
events: Object.assign({}, publicWidget.Widget.prototype.events, {
'click div[name="o_payment_option_card"]': '_onClickPaymentOption',
'click a[name="o_payment_icon_more"]': '_onClickMorePaymentIcons',
'click a[name="o_payment_icon_less"]': '_onClickLessPaymentIcons',
'click button[name="o_payment_submit_button"]': '_onClickSaveToken',
'click button[name="o_payment_delete_token"]': '_onClickDeleteToken',
'submit': '_onSubmit',
}),
/**
* @constructor
*/
init: function () {
const preventDoubleClick = handlerMethod => {
return _.debounce(handlerMethod, 500, true);
};
this._super(...arguments);
// Prevent double-clicks and browser glitches on all inputs
this._onClickDeleteToken = preventDoubleClick(this._onClickDeleteToken);
this._onClickLessPaymentIcons = preventDoubleClick(this._onClickLessPaymentIcons);
this._onClickMorePaymentIcons = preventDoubleClick(this._onClickMorePaymentIcons);
this._onClickPaymentOption = preventDoubleClick(this._onClickPaymentOption);
this._onClickSaveToken = preventDoubleClick(this._onClickSaveToken);
this._onSubmit = preventDoubleClick(this._onSubmit);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Assign the token to a record.
*
* @private
* @param {number} tokenId - The id of the token to assign
* @return {undefined}
*/
_assignToken: function (tokenId) {
// Call the assign route to assign the token to a record
this._rpc({
route: this.txContext.assignTokenRoute,
params: {
'access_token': this.txContext.accessToken,
'token_id': tokenId,
}
}).then(() => {
window.location = this.txContext.landingRoute;
}).guardedCatch(error => {
error.event.preventDefault();
this._displayError(
_t("Server Error"),
_t("We are not able to save your payment method."),
error.message.data.message
);
});
},
/**
* Build the confirmation dialog based on the linked records' information.
*
* @private
* @param {Array} linkedRecordsInfo - The list of information about linked records.
* @param confirmCallback - The callback method called when the user clicks on the
* confirmation button.
* @return {object}
*/
_buildConfirmationDialog: function (linkedRecordsInfo, confirmCallback) {
const $dialogContentMessage = $(
'<span>', {text: _t("Are you sure you want to delete this payment method?")}
);
if (linkedRecordsInfo.length > 0) { // List the documents linked to the token.
$dialogContentMessage.append($('<br>'));
$dialogContentMessage.append($(
'<span>', {text: _t("It is currently linked to the following documents:")}
));
const $documentInfoList = $('<ul>');
linkedRecordsInfo.forEach(documentInfo => {
$documentInfoList.append($('<li>').append($(
'<a>', {
href: documentInfo.url,
target: '_blank',
title: documentInfo.description,
text: documentInfo.name
}
)));
});
$dialogContentMessage.append($documentInfoList);
}
return new Dialog(this, {
title: _t("Warning!"),
size: 'medium',
$content: $('<div>').append($dialogContentMessage),
buttons: [
{
text: _t("Confirm Deletion"), classes: 'btn-primary', close: true,
click: confirmCallback,
},
{
text: _t("Cancel"), close: true
},
],
});
},
/**
* Search for documents linked to the token and ask the user for confirmation.
*
* If any such document is found, a confirmation dialog is shown.
*
* @private
* @param {number} tokenId - The id of the token to delete
* @return {undefined}
*/
_deleteToken: function (tokenId) {
const execute = () => {
this._rpc({
route: '/payment/archive_token',
params: {
'token_id': tokenId,
},
}).then(() => {
const $tokenCard = this.$(
`input[name="o_payment_radio"][data-payment-option-id="${tokenId}"]` +
`[data-payment-option-type="token"]`
).closest('div[name="o_payment_option_card"]');
$tokenCard.siblings(`#o_payment_token_inline_form_${tokenId}`).remove();
$tokenCard.remove();
this._disableButton(false);
}).guardedCatch(error => {
error.event.preventDefault();
this._displayError(
_t("Server Error"),
_t("We are not able to delete your payment method."),
error.message.data.message
);
});
};
// Fetch documents linked to the token
this._rpc({
model: 'payment.token',
method: 'get_linked_records_info',
args: [tokenId],
}).then(linkedRecordsInfo => {
this._buildConfirmationDialog(linkedRecordsInfo, execute).open();
}).guardedCatch(error => {
this._displayError(
_t("Server Error"),
_t("We are not able to delete your payment method."),
error.message.data.message
);
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Find the radio button linked to the click 'Delete' button and trigger the token deletion.
*
* Let `_onClickPaymentOption` select the radio button and display the inline form.
*
* Called when clicking on the 'Delete' button of a token.
*
* @private
* @param {Event} ev
* @return {undefined}
*/
_onClickDeleteToken: function (ev) {
ev.preventDefault();
// Extract contextual values from the delete button
const linkedRadio = $(ev.currentTarget).siblings().find('input[name="o_payment_radio"]')[0];
const tokenId = this._getPaymentOptionIdFromRadio(linkedRadio);
// Delete the token
this._deleteToken(tokenId);
},
/**
* Handle the creation of a new token or the assignation of a token to a record.
*
* Called when clicking on the 'Save Payment Method' button of when submitting the form.
*
* @private
* @param {Event} ev
* @return {undefined}
*/
_onClickSaveToken: async function (ev) {
ev.stopPropagation();
ev.preventDefault();
// Check that the user has selected a payment option
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
if (!this._ensureRadioIsChecked($checkedRadios)) {
return;
}
const checkedRadio = $checkedRadios[0];
// Extract contextual values from the radio button
const provider = this._getProviderFromRadio(checkedRadio);
const paymentOptionId = this._getPaymentOptionIdFromRadio(checkedRadio);
const flow = this._getPaymentFlowFromRadio(checkedRadio);
// Save the payment method
this._hideError(); // Don't keep the error displayed if the user is going through 3DS2
this._disableButton(true); // Disable until it is needed again
$('body').block({
message: false,
overlayCSS: {backgroundColor: "#000", opacity: 0, zIndex: 1050},
});
if (flow !== 'token') { // Creation of a new token
this.txContext.tokenizationRequested = true;
this.txContext.isValidation = true;
this._processPayment(provider, paymentOptionId, flow);
} else if (this.txContext.allowTokenSelection) { // Assignation of a token to a record
this._assignToken(paymentOptionId);
}
},
/**
* Delegate the handling of the token to `_onClickSaveToken`.
*
* Called when submitting the form (e.g. through the Return key).
*
* @private
* @param {Event} ev
* @return {undefined}
*/
_onSubmit: function (ev) {
ev.stopPropagation();
ev.preventDefault();
this._onClickSaveToken(ev);
},
});
return publicWidget.registry.PaymentManageForm;
});

View file

@ -0,0 +1,533 @@
odoo.define('payment.payment_form_mixin', require => {
'use strict';
const core = require('web.core');
const Dialog = require('web.Dialog');
const _t = core._t;
return {
/**
* @override
*/
start: async function () {
this.txContext = {}; // Synchronously initialize txContext before any await.
Object.assign(this.txContext, this.$el.data());
await this._super(...arguments);
window.addEventListener('pageshow', function (event) {
if (event.persisted) {
window.location.reload();
}
});
this.$('[data-bs-toggle="tooltip"]').tooltip();
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
if ($checkedRadios.length === 1) {
const checkedRadio = $checkedRadios[0];
this._displayInlineForm(checkedRadio);
this._enableButton();
} else {
this._setPaymentFlow(); // Initialize the payment flow to let providers overwrite it
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Disable the submit button.
*
* The icons are updated to either show that an action is processing or that the button is
* not ready, depending on the value of `showLoadingAnimation`.
*
* @private
* @param {boolean} showLoadingAnimation - Whether a spinning loader should be shown
* @return {undefined}
*/
_disableButton: function (showLoadingAnimation = true) {
const $submitButton = this.$('button[name="o_payment_submit_button"]');
const iconClass = $submitButton.data('icon-class');
$submitButton.attr('disabled', true);
if (showLoadingAnimation) {
$submitButton.find('i').removeClass(iconClass);
$submitButton.prepend(
'<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>'
);
}
},
/**
* Display an error in the payment form.
*
* If no payment option is selected, the error is displayed in a dialog. If exactly one
* payment option is selected, the error is displayed in the inline form of that payment
* option and the view is focused on the error.
*
* @private
* @param {string} title - The title of the error
* @param {string} description - The description of the error
* @param {string} error - The raw error message
* @return {(Dialog|undefined)} A dialog showing the error if no payment option is selected,
* undefined otherwise.
*/
_displayError: function (title, description = '', error = '') {
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
if ($checkedRadios.length !== 1) { // Cannot find selected payment option, show dialog
return new Dialog(null, {
title: _.str.sprintf(_t("Error: %s"), title),
size: 'medium',
$content: `<p>${_.str.escapeHTML(description) || ''}</p>`,
buttons: [{text: _t("Ok"), close: true}]
}).open();
} else { // Show error in inline form
this._hideError(); // Remove any previous error
// Build the html for the error
let errorHtml = `<div class="alert alert-danger mb4" name="o_payment_error">
<b>${_.str.escapeHTML(title)}</b>`;
if (description !== '') {
errorHtml += `</br>${_.str.escapeHTML(description)}`;
}
if (error !== '') {
errorHtml += `</br>${_.str.escapeHTML(error)}`;
}
errorHtml += '</div>';
// Append error to inline form and center the page on the error
const checkedRadio = $checkedRadios[0];
const paymentOptionId = this._getPaymentOptionIdFromRadio(checkedRadio);
const formType = $(checkedRadio).data('payment-option-type');
const $inlineForm = this.$(`#o_payment_${formType}_inline_form_${paymentOptionId}`);
$inlineForm.removeClass('d-none'); // Show the inline form even if it was empty
$inlineForm.append(errorHtml).find('div[name="o_payment_error"]')[0]
.scrollIntoView({behavior: 'smooth', block: 'center'});
}
this._enableButton(); // Enable button back after it was disabled before processing
$('body').unblock(); // The page is blocked at this point, unblock it
},
/**
* Display the inline form of the selected payment option and hide others.
*
* @private
* @param {HTMLInputElement} radio - The radio button linked to the payment option
* @return {undefined}
*/
_displayInlineForm: function (radio) {
this._hideInlineForms(); // Collapse previously opened inline forms
this._hideError(); // The error is only relevant until it is hidden with its inline form
this._setPaymentFlow(); // Reset the payment flow to let providers overwrite it
// Extract contextual values from the radio button
const provider = this._getProviderFromRadio(radio);
const paymentOptionId = this._getPaymentOptionIdFromRadio(radio);
const flow = this._getPaymentFlowFromRadio(radio);
// Prepare the inline form of the selected payment option and display it if not empty
this._prepareInlineForm(provider, paymentOptionId, flow);
const formType = $(radio).data('payment-option-type');
const $inlineForm = this.$(`#o_payment_${formType}_inline_form_${paymentOptionId}`);
if ($inlineForm.children().length > 0) {
$inlineForm.removeClass('d-none');
}
},
/**
* Check if the submit button can be enabled and do it if so.
*
* The icons are updated to show that the button is ready.
*
* @private
* @return {boolean} Whether the button was enabled.
*/
_enableButton: function () {
if (this._isButtonReady()) {
const $submitButton = this.$('button[name="o_payment_submit_button"]');
const iconClass = $submitButton.data('icon-class');
$submitButton.attr('disabled', false);
$submitButton.find('i').addClass(iconClass);
$submitButton.find('span.o_loader').remove();
return true;
}
return false;
},
/**
* Verify that exactly one radio button is checked and display an error otherwise.
*
* @private
* @param {jQuery} $checkedRadios - The currently check radio buttons
*
* @return {boolean} Whether exactly one radio button among the provided radios is checked
*/
_ensureRadioIsChecked: function ($checkedRadios) {
if ($checkedRadios.length === 0) {
this._displayError(
_t("No payment option selected"),
_t("Please select a payment option.")
);
return false;
} else if ($checkedRadios.length > 1) {
this._displayError(
_t("Multiple payment options selected"),
_t("Please select only one payment option.")
);
return false;
}
return true;
},
/**
* Determine and return the online payment flow of the selected payment option.
*
* As some providers implement both the direct payment and the payment with redirection, the
* flow cannot be inferred from the radio button only. The radio button only indicates
* whether the payment option is a token. If not, the transaction context is looked up to
* determine whether the flow is 'direct' or 'redirect'.
*
* @private
* @param {HTMLInputElement} radio - The radio button linked to the payment option
* @return {string} The flow of the selected payment option. redirect, direct or token.
*/
_getPaymentFlowFromRadio: function (radio) {
if (
$(radio).data('payment-option-type') === 'token'
|| this.txContext.flow === 'token'
) {
return 'token';
} else if (this.txContext.flow === 'redirect') {
return 'redirect';
} else {
return 'direct';
}
},
/**
* Determine and return the id of the selected payment option.
*
* @private
* @param {HTMLInputElement} radio - The radio button linked to the payment option
* @return {number} The provider id or the token id or of the payment option linked to the
* radio button.
*/
_getPaymentOptionIdFromRadio: radio => $(radio).data('payment-option-id'),
/**
* Determine and return the provider of the selected payment option.
*
* @private
* @param {HTMLInputElement} radio - The radio button linked to the payment option
* @return {number} The provider of the payment option linked to the radio button.
*/
_getProviderFromRadio: radio => $(radio).data('provider'),
/**
* Remove the error in the provider form.
*
* @private
* @return {jQuery} The removed error
*/
_hideError: () => this.$('div[name="o_payment_error"]').remove(),
/**
* Collapse all inline forms.
*
* @private
* @return {undefined}.
*/
_hideInlineForms: () => this.$('[name="o_payment_inline_form"]').addClass('d-none'),
/**
* Hide the "Save my payment details" label and checkbox, and the submit button.
*
* The inputs should typically be hidden when the customer has to perform additional actions
* in the inline form. All inputs are automatically shown again when the customer clicks on
* another inline form.
*
* @private
* @return {undefined}
*/
_hideInputs: function () {
const $submitButton = this.$('button[name="o_payment_submit_button"]');
const $tokenizeCheckboxes = this.$('input[name="o_payment_save_as_token"]');
$submitButton.addClass('d-none');
$tokenizeCheckboxes.closest('label').addClass('d-none');
},
/**
* Verify that the submit button is ready to be enabled.
*
* For a module to support a custom behavior for the submit button, it must override this
* method and only return true if the result of this method is true and if nothing prevents
* enabling the submit button for that custom behavior.
*
* @private
*
* @return {boolean} Whether the submit button can be enabled
*/
_isButtonReady: function () {
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
if ($checkedRadios.length === 1) {
const checkedRadio = $checkedRadios[0];
const flow = this._getPaymentFlowFromRadio(checkedRadio);
return flow !== 'token' || this.txContext.allowTokenSelection;
} else {
return false;
}
},
/**
* Prepare the params to send to the transaction route.
*
* For a provider to overwrite generic params or to add provider-specific ones, it must
* override this method and return the extended transaction route params.
*
* @private
* @param {string} code - The code of the selected payment option provider
* @param {number} paymentOptionId - The id of the selected payment option
* @param {string} flow - The online payment flow of the selected payment option
* @return {object} The transaction route params
*/
_prepareTransactionRouteParams: function (code, paymentOptionId, flow) {
return {
'payment_option_id': paymentOptionId,
'reference_prefix': this.txContext.referencePrefix !== undefined
? this.txContext.referencePrefix.toString() : null,
'amount': this.txContext.amount !== undefined
? parseFloat(this.txContext.amount) : null,
'currency_id': this.txContext.currencyId
? parseInt(this.txContext.currencyId) : null,
'partner_id': parseInt(this.txContext.partnerId),
'flow': flow,
'tokenization_requested': this.txContext.tokenizationRequested,
'landing_route': this.txContext.landingRoute,
'is_validation': this.txContext.isValidation,
'access_token': this.txContext.accessToken
? this.txContext.accessToken : undefined,
'csrf_token': core.csrf_token,
};
},
/**
* Prepare the provider-specific inline form of the selected payment option.
*
* For a provider to manage an inline form, it must override this method. When the override
* is called, it must lookup the parameters to decide whether it is necessary to prepare its
* inline form. Otherwise, the call must be sent back to the parent method.
*
* @private
* @param {string} code - The code of the selected payment option's provider
* @param {number} paymentOptionId - The id of the selected payment option
* @param {string} flow - The online payment flow of the selected payment option
* @return {Promise}
*/
_prepareInlineForm: (code, paymentOptionId, flow) => Promise.resolve(),
/**
* Process the payment.
*
* For a provider to do pre-processing work on the transaction processing flow, or to
* define its entire own flow that requires re-scheduling the RPC to the transaction route,
* it must override this method.
* If only post-processing work is needed, an override of `_processRedirectPayment`,
* `_processDirectPayment` or `_processTokenPayment` might be more appropriate.
*
* @private
* @param {string} code - The code of the payment option's provider
* @param {number} paymentOptionId - The id of the payment option handling the transaction
* @param {string} flow - The online payment flow of the transaction
* @return {Promise}
*/
_processPayment: function (code, paymentOptionId, flow) {
// Call the transaction route to create a tx and retrieve the processing values
return this._rpc({
route: this.txContext.transactionRoute,
params: this._prepareTransactionRouteParams(code, paymentOptionId, flow),
}).then(processingValues => {
if (flow === 'redirect') {
return this._processRedirectPayment(
code, paymentOptionId, processingValues
);
} else if (flow === 'direct') {
return this._processDirectPayment(code, paymentOptionId, processingValues);
} else if (flow === 'token') {
return this._processTokenPayment(code, paymentOptionId, processingValues);
}
}).guardedCatch(error => {
error.event.preventDefault();
this._displayError(
_t("Server Error"),
_t("We are not able to process your payment."),
error.message.data.message
);
});
},
/**
* Execute the provider-specific implementation of the direct payment flow.
*
* For a provider to redefine the processing of the direct payment flow, it must override
* this method.
*
* @private
* @param {string} code - The code of the provider
* @param {number} providerId - The id of the provider handling the transaction
* @param {object} processingValues - The processing values of the transaction
* @return {Promise}
*/
_processDirectPayment: (code, providerId, processingValues) => Promise.resolve(),
/**
* Redirect the customer by submitting the redirect form included in the processing values.
*
* For a provider to redefine the processing of the payment with redirection flow, it must
* override this method.
*
* @private
* @param {string} code - The code of the provider
* @param {number} providerId - The id of the provider handling the transaction
* @param {object} processingValues - The processing values of the transaction
* @return {undefined}
*/
_processRedirectPayment: (code, providerId, processingValues) => {
// Append the redirect form to the body
const $redirectForm = $(processingValues.redirect_form_html).attr(
'id', 'o_payment_redirect_form'
);
// Ensures external redirections when in an iframe.
$redirectForm[0].setAttribute('target', '_top');
$(document.getElementsByTagName('body')[0]).append($redirectForm);
// Submit the form
$redirectForm.submit();
},
/**
* Redirect the customer to the status route.
*
* For a provider to redefine the processing of the payment by token flow, it must override
* this method.
*
* @private
* @param {string} provider_code - The code of the token's provider
* @param {number} tokenId - The id of the token handling the transaction
* @param {object} processingValues - The processing values of the transaction
* @return {undefined}
*/
_processTokenPayment: (provider_code, tokenId, processingValues) => {
// The flow is already completed as payments by tokens are immediately processed
window.location = '/payment/status';
},
/**
* Set the online payment flow for the selected payment option.
*
* For a provider to manage direct payments, it must call this method from within its
* override of `_prepareInlineForm` to declare its payment flow for the selected payment
* option.
*
* @private
* @param {string} flow - The flow for the selected payment option. Either 'redirect',
* 'direct' or 'token'
* @return {undefined}
*/
_setPaymentFlow: function (flow = 'redirect') {
if (flow !== 'redirect' && flow !== 'direct' && flow !== 'token') {
console.warn(
`payment_form_mixin: method '_setPaymentFlow' was called with invalid flow:
${flow}. Falling back to 'redirect'.`
);
this.txContext.flow = 'redirect';
} else {
this.txContext.flow = flow;
}
},
/**
* Show the "Save my payment details" label and checkbox, and the submit button.
*
* @private
* @return {undefined}.
*/
_showInputs: function () {
const $submitButton = this.$('button[name="o_payment_submit_button"]');
const $tokenizeCheckboxes = this.$('input[name="o_payment_save_as_token"]');
$submitButton.removeClass('d-none');
$tokenizeCheckboxes.closest('label').removeClass('d-none');
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Hide all extra payment icons of the provider linked to the clicked button.
*
* Called when clicking on the "show less" button.
*
* @private
* @param {Event} ev
* @return {undefined}
*/
_onClickLessPaymentIcons: ev => {
ev.preventDefault();
ev.stopPropagation();
// Hide the extra payment icons, and the "show less" button
const $itemList = $(ev.currentTarget).parents('ul');
const maxIconNumber = $itemList.data('max-icons');
$itemList.children('li').slice(maxIconNumber).addClass('d-none');
// Show the "show more" button
$itemList.find('a[name="o_payment_icon_more"]').parents('li').removeClass('d-none');
},
/**
* Display all the payment icons of the provider linked to the clicked button.
*
* Called when clicking on the "show more" button.
*
* @private
* @param {Event} ev
* @return {undefined}
*/
_onClickMorePaymentIcons: ev => {
ev.preventDefault();
ev.stopPropagation();
// Display all the payment icons, and the "show less" button
$(ev.currentTarget).parents('ul').children('li').removeClass('d-none');
// Hide the "show more" button
$(ev.currentTarget).parents('li').addClass('d-none');
},
/**
* Mark the clicked card radio button as checked and open the inline form, if any.
*
* Called when clicking on the card of a payment option.
*
* @private
* @param {Event} ev
* @return {undefined}
*/
_onClickPaymentOption: function (ev) {
// Uncheck all radio buttons
this.$('input[name="o_payment_radio"]').prop('checked', false);
// Check radio button linked to selected payment option
const checkedRadio = $(ev.currentTarget).find('input[name="o_payment_radio"]')[0];
$(checkedRadio).prop('checked', true);
// Show the inputs in case they had been hidden
this._showInputs();
// Disable the submit button while building the content
this._disableButton(false);
// Unfold and prepare the inline form of selected payment option
this._displayInlineForm(checkedRadio);
// Re-enable the submit button
this._enableButton();
},
};
});

View file

@ -0,0 +1,142 @@
odoo.define('payment.post_processing', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
var core = require('web.core');
const {Markup} = require('web.utils');
var _t = core._t;
$.blockUI.defaults.css.border = '0';
$.blockUI.defaults.css["background-color"] = '';
$.blockUI.defaults.overlayCSS["opacity"] = '0.9';
publicWidget.registry.PaymentPostProcessing = publicWidget.Widget.extend({
selector: 'div[name="o_payment_status"]',
_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;
this._rpc({
route: '/payment/status/poll',
params: {
'csrf_token': core.csrf_token,
}
}).then(function(data) {
if(data.success === true) {
self.processPolledData(data.display_values_list);
}
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 (display_values_list) {
var render_values = {
'tx_draft': [],
'tx_pending': [],
'tx_authorized': [],
'tx_done': [],
'tx_cancel': [],
'tx_error': [],
};
// group the transaction according to their state
display_values_list.forEach(function (display_values) {
var key = 'tx_' + display_values.state;
if(key in render_values) {
if (display_values["display_message"]) {
display_values.display_message = Markup(display_values.display_message)
}
render_values[key].push(display_values);
}
});
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;
}
/*
* When the server sends the list of monitored transactions, it tries to post-process
* all the successful ones. If it succeeds or if the post-process has already been made,
* the transaction is removed from the list of monitored transactions and won't be
* included in the next response. We assume that successful and post-process
* transactions should always prevail on others, regardless of their number or state.
*/
if (render_values['tx_done'].length === 1 &&
render_values['tx_done'][0].is_post_processed) {
window.location = render_values['tx_done'][0].landing_route;
return;
}
// If there are multiple transactions monitored, display them all to the customer. If
// there is only one transaction monitored, redirect directly the customer to the
// landing route.
if(countTxInState(['tx_done', 'tx_error', 'tx_pending', 'tx_authorized']) === 1) {
// We don't want to redirect customers to the landing page when they have a pending
// transaction. The successful transactions are dealt with before.
var tx = render_values['tx_authorized'][0] || render_values['tx_error'][0];
if (tx) {
window.location = tx.landing_route;
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('div[name="o_payment_status_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/img/spin.png" class="fa-pulse"/>' +
' <br />' + msg +
'</h2>'
});
},
});
return publicWidget.registry.PaymentPostProcessing;
});

View file

@ -0,0 +1,62 @@
.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 {
border-radius: 5px;
overflow: hidden;
}
.card-body {
&:not(:first-child) {
border-top: 1px solid #dddddd;
}
padding: 1.14em !important;
&.o_payment_option_card:hover {
cursor: pointer;
}
}
.card-footer {
padding: 0.5rem;
label {
margin-top: 15px;
}
}
.card-footer:last-child {
border-bottom-right-radius: 10px !important;
border-bottom-left-radius: 10px !important;
}
.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;
}
}

View file

@ -0,0 +1,20 @@
.o_form_view {
.o_payment_provider_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;
}
}

View file

@ -0,0 +1,3 @@
div#o_payment_status_alert > p {
margin-bottom: 0;
}

View file

@ -0,0 +1,61 @@
input#cc_number {
background-repeat: no-repeat;
background-position: center right calc(2.7em);
}
div.card_placeholder {
background-image: url("/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("/payment/static/src/img/amex.png");
background-repeat: no-repeat;
}
div.diners {
background-image: url("/payment/static/src/img/diners.png");
background-repeat: no-repeat;
}
div.discover {
background-image: url("/payment/static/src/img/discover.png");
background-repeat: no-repeat;
}
div.jcb {
background-image: url("/payment/static/src/img/jcb.png");
background-repeat: no-repeat;
}
div.mastercard {
background-image: url("/payment/static/src/img/mastercard.png");
background-repeat: no-repeat;
}
div.visa {
background-image: url("/payment/static/src/img/visa.png");
background-repeat: no-repeat;
}
ul.checkout img.rounded {
max-width: 100px;
max-height: 40px;
}

View file

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="payment" xml:space="preserve">
<!-- The templates here as rendered by 'post_processing.js', you can also take
a look at payment_portal_templates.xml (xmlid: payment_status) for more infos-->
<t t-name="payment.display_tx_list">
<div>
<!-- Error transactions -->
<div t-if="tx_error.length > 0">
<h1>Failed operations</h1>
<ul class="list-group">
<t t-foreach="tx_error" t-as="tx">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
An error occurred during the processing of this payment.<br/>
<strong>Reason:</strong> <t t-esc="tx['state_message']"/>
</small>
</a>
</t>
</ul>
</div>
<!-- Pending/Authorized/Confirmed transactions -->
<div t-if="tx_done.length > 0 || tx_authorized.length > 0 || tx_pending.length > 0">
<h1>Operations in progress</h1>
<div class="list-group">
<!-- Done transactions -->
<t t-foreach="tx_done" t-as="tx">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
<t t-if="!tx['is_post_processed']">
<t t-if="tx['operation'] != 'validation'">
Your payment is being processed, please wait... <i class="fa fa-cog fa-spin"/>
</t>
<t t-else="">
Saving your payment method, please wait... <i class="fa fa-cog fa-spin"/>
</t>
</t>
<t t-else="">
<t t-if="tx['operation'] != 'validation'">
Your payment has been processed.<br/>
Click here to be redirected to the confirmation page.
</t>
<t t-else="">
Your payment method has been saved.<br/>
Click here to be redirected to the confirmation page.
</t>
</t>
</small>
</a>
</t>
<!-- Pending transactions -->
<t t-foreach="tx_pending" t-as="tx">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
<t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated
with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/>
</t>
<t t-else="">
Your payment is in pending state.<br/>
You will be notified when the payment is fully confirmed.<br/>
Click here to be redirected to the confirmation page.
</t>
</small>
</a>
</t>
<!-- Authorized transactions -->
<t t-foreach="tx_authorized" t-as="tx">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
<t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated
with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/>
</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>
</a>
</t>
</div>
</div>
<!-- Draft transactions -->
<div t-if="tx_draft.length > 0">
<h1>Waiting for operations to process</h1>
<ul class="list-group">
<t t-foreach="tx_draft" t-as="tx">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
<t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated
with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/>
</t>
<t t-else="">
We are waiting for the payment provider to confirm the payment.
</t>
</small>
</a>
</t>
</ul>
</div>
<!-- Cancel transactions -->
<div t-if="tx_cancel.length > 0">
<h1>Canceled operations</h1>
<ul class="list-group">
<t t-foreach="tx_cancel" t-as="tx">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
This payment has been canceled.<br/>
No payment has been processed.<br/>
<t t-if="tx['state_message']">
<strong>Reason:</strong> <t t-esc="tx['state_message']"/>
</t>
</small>
</a>
</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>