1/** 2 * Custom Place Order Button API 3 * 4 * Shared functionality for custom place order buttons across checkout, order-pay, 5 * and add-payment-method pages. This module provides the core registration and 6 * button management logic. 7 * 8 * @since 10.6.0 9 */ 10 11( function ( $ ) { 12 'use strict'; 13 14 // Initialize the global wc.customPlaceOrderButton namespace 15 window.wc = window.wc || {}; 16 window.wc.customPlaceOrderButton = window.wc.customPlaceOrderButton || {}; 17 18 // Inject critical CSS inline to ensure it works even when themes dequeue woocommerce.css 19 ( function injectStyles() { 20 var styleId = 'wc-custom-place-order-button-styles'; 21 if ( document.getElementById( styleId ) ) { 22 return; 23 } 24 var style = document.createElement( 'style' ); 25 style.id = styleId; 26 style.textContent = 'form.has-custom-place-order-button #place_order { display: none !important; }'; 27 document.head.appendChild( style ); 28 } )(); 29 30 /** 31 * Registry for custom place order buttons. 32 * Key: gateway_id, Value: { render: function, cleanup: function } 33 */ 34 var customPlaceOrderButtons = {}; 35 36 /** 37 * Currently active custom button gateway ID, or null if using the default button. 38 */ 39 var activeCustomButtonGateway = null; 40 41 /** 42 * Container element for the custom place order button. 43 */ 44 var $customButtonContainer = null; 45 46 /** 47 * Get the current form element based on page context. 48 * 49 * @return {jQuery} The form element. 50 */ 51 function getForm() { 52 if ( $( 'form.checkout' ).length ) { 53 return $( 'form.checkout' ).first(); 54 } 55 if ( $( '#order_review' ).length ) { 56 return $( '#order_review' ).first(); 57 } 58 if ( $( '#add_payment_method' ).length ) { 59 return $( '#add_payment_method' ).first(); 60 } 61 return $( [] ); 62 } 63 64 /** 65 * Get a list of gateway IDs with custom place order buttons from server config. 66 * 67 * @return {Array} List of gateway IDs 68 */ 69 function getGatewaysWithCustomButton() { 70 // Try multiple param sources for compatibility across pages 71 if ( typeof wc_checkout_params !== 'undefined' && wc_checkout_params.gateways_with_custom_place_order_button ) { 72 return wc_checkout_params.gateways_with_custom_place_order_button; 73 } 74 if ( typeof wc_add_payment_method_params !== 'undefined' && wc_add_payment_method_params.gateways_with_custom_place_order_button ) { 75 return wc_add_payment_method_params.gateways_with_custom_place_order_button; 76 } 77 return []; 78 } 79 80 /** 81 * Check if a gateway has a custom place order button registered via a server-side flag. 82 * 83 * @param {string} gatewayId - The payment gateway ID 84 * @return {boolean} True if gateway has custom button 85 */ 86 function gatewayHasCustomPlaceOrderButton( gatewayId ) { 87 return getGatewaysWithCustomButton().indexOf( gatewayId ) !== -1; 88 } 89 90 /** 91 * Create or get the container for custom place order buttons. 92 * The container is scoped to the current form to handle multiple forms on a page. 93 * We're not creating the container server-side to avoid introducing a new template 94 * that might break compatibility with subscriptions or other extensions. 95 * 96 * @return {jQuery} The container element 97 */ 98 function getOrCreateCustomButtonContainer() { 99 if ( $customButtonContainer && $customButtonContainer.length && $.contains( document, $customButtonContainer[ 0 ] ) ) { 100 return $customButtonContainer; 101 } 102 103 var $placeOrderButton = getForm().find( '#place_order' ); 104 if ( ! $placeOrderButton.length ) { 105 return $( [] ); 106 } 107 108 $customButtonContainer = $( '<div class="wc-custom-place-order-button"></div>' ); 109 $placeOrderButton.after( $customButtonContainer ); 110 111 return $customButtonContainer; 112 } 113 114 /** 115 * Remove the custom button container. 116 */ 117 function removeCustomButtonContainer() { 118 if ( $customButtonContainer && $customButtonContainer.length ) { 119 $customButtonContainer.remove(); 120 $customButtonContainer = null; 121 } 122 } 123 124 /** 125 * Clean up the current custom button if any. 126 */ 127 function cleanupCurrentCustomButton() { 128 if ( activeCustomButtonGateway && customPlaceOrderButtons[ activeCustomButtonGateway ] ) { 129 try { 130 customPlaceOrderButtons[ activeCustomButtonGateway ].cleanup(); 131 } catch ( e ) { 132 // Log errors to help gateway developers debug their cleanup implementation. 133 // eslint-disable-next-line no-console 134 console.error( 'Error in custom place order button cleanup:', e ); 135 } 136 } 137 removeCustomButtonContainer(); 138 activeCustomButtonGateway = null; 139 } 140 141 /** 142 * Show custom place order button for a gateway, or show default button. 143 * The `api` object is needed because the add-payment-method page is different than a checkout or a pay-for-order page. 144 * Each page can decide how to implement the `validate` and `submit` methods. 145 * 146 * @param {string} gatewayId - The payment gateway ID 147 * @param {Object} api - The API object to pass to render callback 148 */ 149 function maybeShowCustomPlaceOrderButton( gatewayId, api ) { 150 var $form = getForm(); 151 152 // Clean up any displayed custom button, if any 153 if ( activeCustomButtonGateway && customPlaceOrderButtons[ activeCustomButtonGateway ] ) { 154 try { 155 customPlaceOrderButtons[ activeCustomButtonGateway ].cleanup(); 156 } catch ( e ) { 157 // Log errors to help gateway developers debug their cleanup implementation. 158 // eslint-disable-next-line no-console 159 console.error( 'Error in custom place order button cleanup:', e ); 160 } 161 } 162 163 var isCustomButtonRegistered = Boolean( customPlaceOrderButtons[ gatewayId ] ); 164 if ( isCustomButtonRegistered ) { 165 // Hide the default button and show the custom one, instead. 166 $form.addClass( 'has-custom-place-order-button' ); 167 activeCustomButtonGateway = gatewayId; 168 169 var $container = getOrCreateCustomButtonContainer(); 170 $container.empty(); 171 172 try { 173 customPlaceOrderButtons[ gatewayId ].render( $container.get( 0 ), api ); 174 } catch ( e ) { 175 // Log errors to help gateway developers debug their render implementation. 176 // eslint-disable-next-line no-console 177 console.error( 'Error rendering custom place order button:', e ); 178 } 179 } else { 180 // Only show default button if gateway doesn't have a custom button pending registration. 181 // This prevents flash when gateway JS loads after the initial click. 182 // Basically, when `__maybeShow` is called for a gateway that isn't registered yet but has the server-side flag set, 183 // we keep the default button hidden instead of flashing it. 184 if ( ! gatewayHasCustomPlaceOrderButton( gatewayId ) ) { 185 $form.removeClass( 'has-custom-place-order-button' ); 186 } 187 activeCustomButtonGateway = null; 188 removeCustomButtonContainer(); 189 } 190 } 191
192 /** 193 * Hide default button immediately if selected gateway has custom button (prevents flash). 194 * 195 * @param {string} gatewayId - The payment gateway ID 196 */ 197 function maybeHideDefaultButtonOnInit( gatewayId ) { 198 if ( gatewayHasCustomPlaceOrderButton( gatewayId ) ) { 199 var $form = getForm(); 200 $form.addClass( 'has-custom-place-order-button' ); 201 } 202 } 203 204 /** 205 * Register a custom place order button for a payment gateway. 206 * 207 * @param {string} gatewayId - The payment gateway ID (e.g., 'google_pay') 208 * @param {Object} config - Configuration object 209 * @param {Function} config.render - Function called to render the button. Receives (container, api) 210 * @param {Function} config.cleanup - Function called when switching away from this gateway 211 */ 212 function registerCustomPlaceOrderButton( gatewayId, config ) { 213 // Silently ignore if already registered (prevents double-registration issues) 214 if ( customPlaceOrderButtons[ gatewayId ] ) { 215 return; 216 } 217 218 if ( typeof gatewayId !== 'string' || ! gatewayId ) { 219 // Log validation errors to help gateway developers fix incorrect API usage. 220 // eslint-disable-next-line no-console 221 console.error( 'wc.customPlaceOrderButton.register: gatewayId must be a non-empty string' ); 222 return; 223 } 224 if ( typeof config !== 'object' || config === null ) { 225 // Log validation errors to help gateway developers fix incorrect API usage. 226 // eslint-disable-next-line no-console 227 console.error( 'wc.customPlaceOrderButton.register: config must be an object' ); 228 return; 229 } 230 if ( typeof config.render !== 'function' ) { 231 // Log validation errors to help gateway developers fix incorrect API usage. 232 // eslint-disable-next-line no-console 233 console.error( 'wc.customPlaceOrderButton.register: render must be a function' ); 234 return; 235 } 236 if ( typeof config.cleanup !== 'function' ) { 237 // Log validation errors to help gateway developers fix incorrect API usage. 238 // eslint-disable-next-line no-console 239 console.error( 'wc.customPlaceOrderButton.register: cleanup must be a function' ); 240 return; 241 } 242 243 customPlaceOrderButtons[ gatewayId ] = config; 244 245 // If this gateway is already selected, notify that registration is complete 246 if ( getForm().find( 'input[name="payment_method"]:checked' ).val() === gatewayId ) { 247 // since this API needs to be used on checkout/pay for order/my account pages, 248 // we need to trigger a global event to ensure it's picked up by the WC Core JS used in those pages. 249 $( document.body ).trigger( 'wc_custom_place_order_button_registered', [ gatewayId ] ); 250 } 251 } 252 253 // Export functions to the global namespace 254 // Public API (for gateway developers) 255 window.wc.customPlaceOrderButton.register = registerCustomPlaceOrderButton; 256 257 // Internal API (used by WooCommerce core, not for external use) 258 window.wc.customPlaceOrderButton.__maybeShow = maybeShowCustomPlaceOrderButton; 259 window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit = maybeHideDefaultButtonOnInit; 260 window.wc.customPlaceOrderButton.__cleanup = cleanupCurrentCustomButton; 261 window.wc.customPlaceOrderButton.__getForm = getForm; 262 263} )( jQuery );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.