[IMP] website_sale, payment: create payment button widget

Before this commit, the payment button was part of the checkout/manage
form widgets, which caused conflicts when other modules needed to
enable/disable the button depending on custom conditions.

After this commit, the payment button logic is extracted in a separate
widget that handles its enabling after checking if all conditions are
satisfied. Other modules can extend the widget and override
`isButtonReady` to add custom logic.

task-3568861

closes odoo/odoo#142265

X-original-commit: 496c22f67b840a72758640576546ba16b177aafc
Related: odoo/enterprise#50783
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Valeriya(vchu)
2023-11-24 14:59:41 +00:00
committed by Antoine Vandevenne (anv)
parent 71a61fb8d9
commit 671640cf59
11 changed files with 262 additions and 226 deletions
@@ -0,0 +1,81 @@
/** @odoo-module **/
import publicWidget from '@web/legacy/js/public/public_widget';
import { Component } from "@odoo/owl";
publicWidget.registry.PaymentButton = publicWidget.Widget.extend({
selector: 'button[name="o_payment_submit_button"]',
async start() {
await this._super(...arguments);
this.paymentButton = this.el;
this.iconClass = this.paymentButton.dataset.iconClass;
this._enable();
Component.env.bus.addEventListener('enablePaymentButton', this._enable.bind(this));
Component.env.bus.addEventListener('disablePaymentButton',this._disable.bind(this));
Component.env.bus.addEventListener('hidePaymentButton', this._hide.bind(this));
Component.env.bus.addEventListener('showPaymentButton', this._show.bind(this));
},
/**
* Check if the payment button can be enabled and do it if so.
*
* @private
* @return {void}
*/
_enable() {
if (this._canSubmit()) {
this.paymentButton.disabled = false;
}
},
/**
* Check whether the payment form can be submitted, i.e. whether exactly one payment option is
* selected.
*
* For a module to add a condition on the submission of the form, it must override this method
* and return whether both this method's condition and the override method's condition are met.
*
* @private
* @return {boolean} Whether the form can be submitted.
*/
_canSubmit() {
const paymentForm = document.querySelector('#o_payment_form');
if (!paymentForm) { // Payment form is not present.
return true; // Ignore the check.
}
return document.querySelectorAll('input[name="o_payment_radio"]:checked').length === 1;
},
/**
* Disable the payment button.
*
* @private
* @return {void}
*/
_disable() {
this.paymentButton.disabled = true;
},
/**
* Hide the payment button.
*
* @private
* @return {void}
*/
_hide() {
this.paymentButton.classList.add('d-none');
},
/**
* Show the payment button.
*
* @private
* @return {void}
*/
_show() {
this.paymentButton.classList.remove('d-none');
},
});
export default publicWidget.registry.PaymentButton;
+5 -33
View File
@@ -1,5 +1,6 @@
/** @odoo-module **/
import { Component } from '@odoo/owl';
import publicWidget from '@web/legacy/js/public/public_widget';
import { browser } from '@web/core/browser/browser';
import { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';
@@ -168,9 +169,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
* @return {void}
*/
_enableButton(unblockUI = true) {
if (this._canSubmit()) {
this._getSubmitButton().removeAttribute('disabled');
}
Component.env.bus.trigger('enablePaymentButton');
if (unblockUI) {
this.call('ui', 'unblock');
}
@@ -184,7 +183,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
* @return {void}
*/
_disableButton(blockUI = false) {
this._getSubmitButton().setAttribute('disabled', true);
Component.env.bus.trigger('disablePaymentButton');
if (blockUI) {
this.call('ui', 'block');
}
@@ -202,7 +201,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
tokenizeContainer?.classList.remove('d-none');
// Show the submit button.
this._getSubmitButton().classList.remove('d-none');
Component.env.bus.trigger('showPaymentButton');
},
/**
@@ -221,7 +220,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
tokenizeContainer?.classList.add('d-none');
// Hide the submit button.
this._getSubmitButton().classList.add('d-none');
Component.env.bus.trigger('hidePaymentButton');
},
/**
@@ -313,20 +312,6 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
// #=== PAYMENT FLOW ===#
/**
* Check whether the payment form can be submitted, i.e. whether exactly one payment option is
* selected.
*
* For a module to add a condition on the submission of the form, it must override this method
* and return whether both this method's condition and the override method's condition are met.
*
* @private
* @return {boolean} Whether the form can be submitted.
*/
_canSubmit() {
return this.el.querySelectorAll('input[name="o_payment_radio"]:checked').length === 1;
},
/**
* Set the payment flow for the selected payment option.
*
@@ -535,19 +520,6 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
return inlineFormContainer?.querySelector('[name="o_payment_inline_form"]');
},
/**
* Find and return the submit button.
*
* The button is searched in the whole document, rather than only in the current form, to allow
* modules to place it outside the payment form (e.g., eCommerce).
*
* @private
* @return {Element} The submit button.
*/
_getSubmitButton() {
return document.querySelector('[name="o_payment_submit_button"]');
},
/**
* Determine and return the payment flow of the selected payment option.
*
+3 -1
View File
@@ -61,7 +61,10 @@
'website_sale/static/src/scss/website_sale_delivery.scss',
'website/static/lib/multirange/multirange_custom.scss',
'sale/static/src/scss/sale_portal.scss',
'website_sale/static/src/js/payment_button.js',
'website_sale/static/src/js/payment_form.js',
'website_sale/static/src/js/sale_variant_mixin.js',
'website_sale/static/src/js/terms_and_conditions_checkbox.js',
'website_sale/static/src/js/variant_mixin.js',
'website_sale/static/src/js/website_sale.js',
'website_sale/static/src/xml/website_sale.xml',
@@ -69,7 +72,6 @@
'website_sale/static/src/js/website_sale_price_range_option.js',
'website_sale/static/src/js/website_sale_utils.js',
'website_sale/static/src/xml/website_sale_utils.xml',
'website_sale/static/src/js/website_sale_payment.js',
'website_sale/static/src/js/website_sale_recently_viewed.js',
'website_sale/static/src/js/website_sale_tracking.js',
'website/static/lib/multirange/multirange_custom.js',
@@ -0,0 +1,69 @@
/** @odoo-module **/
import paymentButton from '@payment/js/payment_button';
paymentButton.include({
/**
* Verify that the payment button is ready to be enabled.
*
* The conditions are that:
* - a delivery carrier is selected and ready (the price is computed) if deliveries are enabled;
* - the "Terms and Conditions" checkbox is ticked if it is present.
*
* @override from @payment/js/payment_button
* @return {boolean}
*/
_canSubmit() {
return this._super(...arguments) && this._isCarrierReady() && this._isTCCheckboxReady();
},
/**
* Check if the delivery carrier is selected and if its price is computed.
*
* @private
* @return {boolean}
*/
_isCarrierReady() {
const carriers = document.querySelectorAll('.o_delivery_carrier_select');
if (carriers.length === 0) { // No carrier is available.
return true; // Ignore the check.
}
const checkedCarriers = document.querySelectorAll('input[name="delivery_type"]:checked');
if (checkedCarriers.length === 0) { // No carrier is selected.
return false; // Nothing else to check.
}
const carriersContainer = checkedCarriers[0].closest('.o_delivery_carrier_select');
const isPickUpPointRequired = carriersContainer.querySelector('.o_show_pickup_locations');
if (isPickUpPointRequired) {
const address = carriersContainer.querySelector(
'.o_order_location_address'
).innerText;
const isPickUp = carriersContainer.lastChild.previousSibling.children;
if (
isPickUp.length > 1 && (address === '' || isPickUp[0].classList.contains('d-none'))
) { // A pickup point is required but not selected
return false;
}
}
return true;
},
/**
* Check if the "Terms and Conditions" checkbox is ticked, if present.
*
* @private
* @return {boolean}
*/
_isTCCheckboxReady() {
const checkbox = document.querySelector('#website_sale_tc_checkbox');
if (!checkbox) { // The checkbox is not present.
return true; // Ignore the check.
}
return checkbox.checked;
},
});
@@ -0,0 +1,17 @@
/** @odoo-module **/
import PaymentForm from '@payment/js/payment_form';
PaymentForm.include({
/**
* Create an event listener for the payment button located outside the payment form.
* @override
*/
async start() {
const submitButton = document.querySelector('[name="o_payment_submit_button"]');
submitButton.addEventListener('click', ev => this._submitForm(ev));
return await this._super(...arguments);
}
});
@@ -0,0 +1,34 @@
/** @odoo-module **/
import { Component } from '@odoo/owl';
import publicWidget from '@web/legacy/js/public/public_widget';
publicWidget.registry.TermsAndConditionsCheckbox = publicWidget.Widget.extend({
selector: 'div[name="website_sale_terms_and_conditions_checkbox"]',
events: {
'change #website_sale_tc_checkbox': '_onClickTCCheckbox',
},
async start() {
this.checkbox = this.el.querySelector('#website_sale_tc_checkbox');
return this._super(...arguments);
},
/**
* Enable/disabled the payment button when the "Terms and Conditions" checkbox is
* checked/unchecked.
*
* @private
* @return {void}
*/
_onClickTCCheckbox() {
if (this.checkbox.checked) {
Component.env.bus.trigger('enablePaymentButton');
} else {
Component.env.bus.trigger('disablePaymentButton');
}
},
});
export default publicWidget.registry.TermsAndConditionsCheckbox;
@@ -192,9 +192,14 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
}
},
/**
* Disable the payment button.
*
* @private
* @return {void}
*/
_disablePayButton: function (){
var payButton = document.querySelector('button[name="o_payment_submit_button"]');
payButton? payButton.disabled = true : null;
Component.env.bus.trigger('disablePaymentButton');
},
_disablePayButtonNoPickupPoint : function (ev){
@@ -247,57 +252,16 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
this._enableButton(result.status);
},
/**
* Check if the submit button can be enabled.
* If it is the case, trigger the `_enableButton` from payment
* Enable the payment button if the rate_shipment request succeeded.
*
* @private
* @param {boolean} status - The status of the rate_shipment request.
* @return {void}
*/
_enableButton: function(status){
if (!this._isPayable(status)) {
return;
_enableButton(status){
if (status) {
Component.env.bus.trigger('enablePaymentButton');
}
Component.env.bus.trigger('enableButton');
},
/**
* @private
* @param {boolean} status : the status of the rate_shipment request
* @return {boolean}
*/
// FYI we don't cover the case where the payement method is not selected because it already throws an error
_isPayable: function(status=false){
// abort if no paybutton
var payButton = document.querySelector('button[name="o_payment_submit_button"]');
if (!payButton) {
return false;
}
// abort if the rating failed
if (!status){
return false;
}
const carriers = Array.from(document.querySelectorAll('.o_delivery_carrier_select'))
let carrierChecked = null;
carriers.forEach((carrier) => {
if (carrier.querySelector('input').checked){
carrierChecked = carrier;
}
})
//abort if no carrier is selected
if (!carrierChecked){
return false;
}
// if the carrier does not need a pickup point
const isPickUpPointNeeded = carrierChecked.querySelector('.o_show_pickup_locations')
if (!isPickUpPointNeeded){
return true;
}
const address = carrierChecked.querySelector('.o_order_location_address').innerText
const isPickUp = carrierChecked.lastChild.previousSibling.children;
if (isPickUp.length > 1 && (address == "" || isPickUp[0].classList.contains("d-none"))) {
return false;
}
return true;
},
_isPickupLocationSelected: function (ev) {
@@ -381,11 +345,14 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
* @param {Event} ev
*/
_onCarrierClick: async function (ev) {
this._disablePayButton();
const radio = ev.currentTarget.closest('.o_delivery_carrier_select').querySelector('input[type="radio"]');
if (radio.checked && !this._shouldDisplayPickupLocations(ev)) {
const radio = ev.currentTarget.closest('.o_delivery_carrier_select').querySelector(
'input[type="radio"]'
);
if (radio.checked) {
return;
}
this._disablePayButton();
this._showLoading(radio);
radio.checked = true;
await this._onClickShowLocations(ev);
@@ -1,127 +0,0 @@
/** @odoo-module **/
import { debounce } from '@web/core/utils/timing';
import publicWidget from '@web/legacy/js/public/public_widget';
import PaymentForm from '@payment/js/payment_form';
const websiteSalePaymentMixin = {
// #=== WIDGET LIFECYCLE ===#
/**
* @override
*/
init() {
this._onClickTCCheckbox = debounce(this._onClickTCCheckbox, 100, true);
this._super(...arguments);
},
prepareInputs() {
this.$checkbox = $(document.getElementById('website_sale_tc_checkbox'));
this.$submitButton = $(document.querySelector('[name="o_payment_submit_button"]'));
this._adaptConfirmButton();
},
/**
* Update the data on the submit button with the status of the Terms and Conditions input.
*
* @private
* @return {void}
*/
_adaptConfirmButton() {
if (this.$checkbox.length > 0) {
const disabledReasons = this.$submitButton.data('disabled_reasons') || {};
disabledReasons.tc = !this.$checkbox.prop('checked');
this.$submitButton.data('disabled_reasons', disabledReasons);
}
},
};
PaymentForm.include(Object.assign({}, websiteSalePaymentMixin, {
// #=== WIDGET LIFECYCLE ===#
/**
* @override
*/
async start() {
this.prepareInputs();
if (this.$checkbox.length > 0) { // The checkbox is outside the payment form.
this.$checkbox[0].addEventListener('change', () => this._onClickTCCheckbox());
}
if (this.$submitButton.length > 0) { // The button is outside the payment form.
this.$submitButton[0].addEventListener('click', ev => this._submitForm(ev));
}
return await this._super(...arguments);
},
// #=== EVENT HANDLERS ===#
/**
* Enable the submit button if it all conditions are met.
*
* @private
* @return {void}
*/
_onClickTCCheckbox() {
this._disableButton();
this._adaptConfirmButton();
this._enableButton();
},
// #=== PAYMENT FLOW ===#
/**
* Verify that the Terms and Condition checkbox is checked.
*
* @override method from @payment/js/payment_form
* @private
* @return {boolean} Whether the form can be submitted.
*/
_canSubmit() {
const disabledReasonFound = Object.values(
this.$submitButton.data('disabled_reasons') || {}
).includes(true);
return !disabledReasonFound && this._super();
},
}));
publicWidget.registry.WebsiteSalePayment = publicWidget.Widget.extend(
Object.assign({}, websiteSalePaymentMixin, {
selector: 'div[name="o_website_sale_free_cart"]',
events: {
'change #website_sale_tc_checkbox': '_onClickTCCheckbox',
},
// #=== WIDGET LIFECYCLE ===#
/**
* @override
*/
async start() {
this.prepareInputs();
this._onClickTCCheckbox();
return await this._super(...arguments);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Enable the submit button if it all conditions are met.
*
* @private
* @return {void}
*/
_onClickTCCheckbox() {
this._adaptConfirmButton();
const disabledReasonFound = Object.values(
this.$submitButton.data('disabled_reasons') || {}
).includes(true);
this.$submitButton.prop('disabled', disabledReasonFound);
},
}));
+6 -3
View File
@@ -1735,6 +1735,7 @@
- _cta_classes: CSS classes to append on the primary navigation button; default `None`.
- _form_send_navigation: Whether the primary button serves as a submit button for a
form; default `None`.
- hide_payment_button: Whether the payment button should be hidden; default: False.
-->
<!-- Checkout context:
- website_sale_order: The current order.
@@ -1755,12 +1756,12 @@
name="csrf_token"
t-att-value="request.csrf_token()"
t-nocache="The csrf token must always be up to date."/>
<t t-call="payment.submit_button">
<t t-if="not hide_payment_button" t-call="payment.submit_button">
<t t-set="submit_button_label">Confirm Order</t>
</t>
</form>
</div>
<t t-else="" t-call="payment.submit_button"/>
<t t-elif="not hide_payment_button" t-call="payment.submit_button"/>
</t>
<t t-else="">
<a role="button" name="website_sale_main_button"
@@ -1771,7 +1772,7 @@
</a>
</t>
</t>
<div t-attf-class="position-relative #{_form_send_navigation and 'd-flex d-lg-none' or 'd-flex'} w-100 justify-content-center align-items-center my-2 opacity-75">
<div t-if="not hide_payment_button" t-attf-class="position-relative #{_form_send_navigation and 'd-flex d-lg-none' or 'd-flex'} w-100 justify-content-center align-items-center my-2 opacity-75">
<hr class="w-100"/>
<span class="px-3">or</span>
<hr class="w-100"/>
@@ -2088,6 +2089,7 @@
<div class="oe_structure" id="oe_structure_website_sale_payment_2"/>
</t>
<div class="col-12" t-if="errors">
<t t-set="hide_payment_button" t-value="True"/>
<t t-foreach="errors" t-as="error">
<div class="alert alert-danger" t-if="error" role="alert">
<h4>
@@ -2113,6 +2115,7 @@
<t t-call="payment.form"/>
</div>
<div t-else="" class="alert alert-warning">
<t t-set="hide_payment_button" t-value="True"/>
<strong>No suitable payment option could be found.</strong><br/>
<div t-if="not request.env.is_admin()">
If you believe that it is an error, please contact the website administrator.
@@ -0,0 +1,20 @@
/** @odoo-module **/
import paymentButton from '@payment/js/payment_button';
paymentButton.include({
/**
* Verify that the payment button is ready to be enabled.
*
* The condition is that:
* - The warning onsite is hidden.
*
* @override from @payment/js/payment_button
* @return {boolean}
*/
_canSubmit() {
return this._super(...arguments) && !document.querySelector('.onsite-warning:not(.d-none)');
}
});
@@ -20,7 +20,7 @@ publicWidget.registry.websiteSaleDelivery.include({
);
boldMsg.classList.add('d-block');
this.warning.prepend(boldMsg);
this.warning.classList.add('alert-warning', 'p-3', 'm-1', 'd-none');
this.warning.classList.add('alert-warning', 'p-3', 'm-1', 'd-none', 'onsite-warning');
this.paymentMethodsContainer = document.querySelector('#payment_method');
this.paymentMethodsContainer.querySelector(
@@ -65,12 +65,10 @@ publicWidget.registry.websiteSaleDelivery.include({
const input = ev.currentTarget.querySelector('input');
let atLeastOneOptionAvailable = false;
// Jquery because the button does not behave nicely with vanilla dataset.
let $payButton = $('button[name="o_payment_submit_button"]');
for (let option of this.paymentOptions) {
if (option.dataset.isOnsite && input.dataset.deliveryType !== 'onsite') {
if (option.checked) { // The payment option was selected.
$payButton.attr('disabled', true); // Reset the submit button.
this._disablePayButton(); // Reset the submit button.
}
this._setEnablePaymentOption(option, false);
} else{
@@ -81,13 +79,13 @@ publicWidget.registry.websiteSaleDelivery.include({
}
}
let disabledReasons = $payButton.data('disabled_reasons') || {};
disabledReasons.noOptionAvailableOnsite = false;
if (!atLeastOneOptionAvailable) {
this.warning.classList.remove('d-none');
disabledReasons.noOptionAvailableOnsite = true;
this._disablePayButton();
}
$payButton.data('disabled_reasons', disabledReasons);
}
else {
this._enableButton();
}
},
});