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: "

" + (_.str.escapeHTML(message) || "") + "

" , buttons: [ {text: _t('Ok'), close: true}]}).open(); } else { // removed if exist error message this.$('#payment_error').remove(); var messageResult = '
'; if (title != '') { messageResult = messageResult + '' + _.str.escapeHTML(title) + ':
'; } messageResult = messageResult + _.str.escapeHTML(message) + '
'; $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(' '); }, 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('
' + _.str.escapeHTML("The value is invalid.") + '
'); wrong_input = true; } else if ($(element).closest('div.form-group').hasClass('o_has_error')) { wrong_input = true; $(element).closest('div.form-group').append('
' + _.str.escapeHTML("The value is invalid.") + '
'); } }); 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 = '
' + _.str.escapeHTML("The value is invalid.") + '
'; $(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 = '
' + _.str.escapeHTML("The value is invalid.") + '
'; $(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(' '); // 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 += '

' + sub.name + '

'; }); content = $('
').html('

' + _t('This card is currently linked to the following records:') + '

' + 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; });