PageSourceSearch

https://nefsy.com/wp-content/plugins/checkout-com-unified-payments…/flow-updated-checkout-guard.js?ver=5.1.4.2

js nefsy.com collected 2026-10-04 22:33:10 UTC 33,169 bytes, 843 lines download raw bytes

1/**
2 * Flow updated_checkout Guard Module
3 *
4 * Protects Flow component from WooCommerce updated_checkout DOM replacement.
5 * Key mechanism: Detaches Flow container from DOM before WooCommerce replaces fragments,
6 * then reattaches it after. This preserves card details during coupon operations.
7 *
8 * NOTE: The detach/reattach feature is controlled by admin toggle "Preserve Card Details".
9 * When disabled (default), card details will be cleared on coupon apply for maximum compatibility.
10 *
11 * Dependencies: jQuery, flow-logger.js, flow-state.js
12 *
13 * @module FlowUpdatedCheckoutGuard
14 */
15
16(function () {
17	'use strict';
18
19	// Check if preserve card feature is enabled (admin toggle)
20	const preserveCardEnabled = typeof cko_flow_vars !== 'undefined' && cko_flow_vars.preserve_card_on_update === true;
21
22	// order_amount is in cents (minor units). Used to detect coupon/cart changes so Flow reloads with correct amount.
23	let previousOrderAmount =
24		typeof cko_flow_vars !== 'undefined' && cko_flow_vars.order_amount
25			? parseInt(cko_flow_vars.order_amount, 10)
26			: null;
27
28	let initialized = false;
29	
30	// Storage for detached Flow container during WooCommerce DOM replacement
31	let detachedFlowContainer = null;
32	let detachedFlowParentSelector = null;
33	
34	/**
35	 * Get the current order total in minor units (cents/pence).
36	 * Tries multiple sources to ensure we get the most up-to-date value:
37	 * 1. WooCommerce order total display (most reliable after fragment update)
38	 * 2. Cart-info data attribute (may be stale if not in fragments)
39	 * 
40	 * @returns {number|null} Order total in minor units, or null if not found
41	 */
42	function getCurrentOrderTotalFromDOM() {
43		let currentOrderAmount = null;
44		
45		// Debug: Log all potential price elements
46		if (typeof ckoLogger !== 'undefined') {
47			const allPriceElements = jQuery('.order-total, .cart-subtotal, .cart-discount, .woocommerce-Price-amount');
48			ckoLogger.debug('[ORDER TOTAL] ========== DOM PRICE DEBUG ==========');
49			ckoLogger.debug('[ORDER TOTAL] Found price-related elements:', allPriceElements.length);
50			
51			// Log order-total specifically
52			const orderTotal = jQuery('.order-total');
53			if (orderTotal.length > 0) {
54				ckoLogger.debug('[ORDER TOTAL] .order-total HTML:', orderTotal.html());
55				ckoLogger.debug('[ORDER TOTAL] .order-total text:', orderTotal.text().trim());
56			}
57			
58			// Log if there's a discount row
59			const discountRow = jQuery('.cart-discount');
60			if (discountRow.length > 0) {
61				ckoLogger.debug('[ORDER TOTAL] .cart-discount found:', discountRow.text().trim());
62			}
63		}
64		
65		// Method 1: Read from WooCommerce order total display (most reliable after fragment update)
66		// Try specific order-total first, then fall back to other selectors
67		let orderTotalEl = jQuery('.order-total .woocommerce-Price-amount bdi');
68		
69		if (orderTotalEl.length === 0) {
70			orderTotalEl = jQuery('.order-total .woocommerce-Price-amount');
71		}
72		if (orderTotalEl.length === 0) {
73			orderTotalEl = jQuery('.order-total .amount');
74		}
75		
76		if (orderTotalEl.length > 0) {
77			// Get the LAST price element (in case there are multiple, the final one is usually the actual total)
78			let totalText = orderTotalEl.last().text().trim();
79			// Remove currency symbols and non-numeric chars except decimal
80			let numericValue = totalText.replace(/[^0-9.,]/g, '').replace(',', '.');
81			// Parse and convert to minor units (cents/pence)
82			let parsedValue = parseFloat(numericValue);
83			if (!isNaN(parsedValue)) {
84				currentOrderAmount = Math.round(parsedValue * 100);
85				if (typeof ckoLogger !== 'undefined') {
86					ckoLogger.debug('[ORDER TOTAL] Read from DOM (.order-total):', {
87						displayedText: totalText,
88						parsedValue: parsedValue,
89						minorUnits: currentOrderAmount,
90						elementCount: orderTotalEl.length
91					});
92				}
93				ckoLogger.debug('[ORDER TOTAL] ========== END DOM PRICE DEBUG ==========');
94				return currentOrderAmount;
95			}
96		}
97		
98		// Method 2: Fallback to cart-info data attribute
99		const cartInfo = jQuery('#cart-info').data('cart');
100		if (cartInfo && typeof cartInfo.order_amount !== 'undefined') {
101			currentOrderAmount = parseInt(cartInfo.order_amount, 10);
102			if (typeof ckoLogger !== 'undefined') {
103				ckoLogger.debug('[ORDER TOTAL] Fallback to cart-info data:', {
104					orderAmount: currentOrderAmount
105				});
106				ckoLogger.debug('[ORDER TOTAL] ========== END DOM PRICE DEBUG ==========');
107			}
108			return currentOrderAmount;
109		}
110		
111		if (typeof ckoLogger !== 'undefined') {
112			ckoLogger.debug('[ORDER TOTAL] Could not determine current order total');
113			ckoLogger.debug('[ORDER TOTAL] ========== END DOM PRICE DEBUG ==========');
114		}
115		return null;
116	}
117
118	/**
119	 * Detach Flow container from DOM to protect it from WooCommerce fragment replacement.
120	 * Called BEFORE WooCommerce AJAX request to ensure container is safe.
121	 * Only operates when preserve_card_on_update is enabled.
122	 */
123	function detachFlowContainer() {
124		// Skip if feature is disabled
125		if (!preserveCardEnabled) {
126			if (typeof ckoLogger !== 'undefined') {
127				ckoLogger.debug('[COUPON GUARD] Preserve card feature disabled - skipping detach');
128			}
129			return false;
130		}
131
132		const flowContainer = document.getElementById('flow-container');
133		if (!flowContainer) {
134			if (typeof ckoLogger !== 'undefined') {
135				ckoLogger.debug('[COUPON GUARD] No flow-container to detach');
136			}
137			return false;
138		}
139
140		// Check if Flow is actually mounted (has SDK content)
141		const hasFlowContent = flowContainer.querySelector('[data-testid="checkout-web-component-root"]') ||
142		                       flowContainer.querySelector('.cko-flow-sdk-root') ||
143		                       flowContainer.querySelector('iframe');
144		
145		if (!hasFlowContent) {
146			if (typeof ckoLogger !== 'undefined') {
147				ckoLogger.debug('[COUPON GUARD] Flow container exists but has no SDK content - skipping detach');
148			}
149			return false;
150		}
151
152		// Store reference to parent for reattachment
153		const parent = flowContainer.parentNode;
154		if (parent) {
155			// Create a placeholder to mark where Flow should be reattached
156			const placeholder = document.createElement('div');
157			placeholder.id = 'flow-container-placeholder';
158			placeholder.style.display = 'none';
159			parent.insertBefore(placeholder, flowContainer);
160			
161			// Detach the Flow container (removes from DOM but keeps in memory)
162			detachedFlowContainer = flowContainer;
163			parent.removeChild(flowContainer);
164			
165			if (typeof ckoLogger !== 'undefined') {
166				ckoLogger.debug('[COUPON GUARD] ✅ Flow container DETACHED from DOM (protected from WooCommerce replacement)');
167			}
168			return true;
169		}
170		return false;
171	}
172
173	/**
174	 * Reattach Flow container after WooCommerce finishes DOM updates.
175	 * Only operates when preserve_card_on_update is enabled.
176	 */
177	function reattachFlowContainer() {
178		// Skip if feature is disabled
179		if (!preserveCardEnabled) {
180			return false;
181		}
182
183		if (!detachedFlowContainer) {
184			if (typeof ckoLogger !== 'undefined') {
185				ckoLogger.debug('[COUPON GUARD] No detached container to reattach');
186			}
187			return false;
188		}
189
190		// Find the placeholder or the new payment_box
191		let placeholder = document.getElementById('flow-container-placeholder');
192		let targetParent = null;
193
194		if (placeholder && placeholder.parentNode) {
195			targetParent = placeholder.parentNode;
196			// Remove placeholder and insert Flow container in its place
197			targetParent.insertBefore(detachedFlowContainer, placeholder);
198			placeholder.remove();
199			if (typeof ckoLogger !== 'undefined') {
200				ckoLogger.debug('[COUPON GUARD] ✅ Flow container reattached at placeholder location');
201			}
202		} else {
203			// Placeholder gone (WooCommerce replaced parent) - find new payment_box
204			const paymentMethod = document.querySelector('.payment_method_wc_checkout_com_flow');
205			if (paymentMethod) {
206				const paymentBox = paymentMethod.querySelector('.payment_box');
207				if (paymentBox) {
208					// Clear any new content WooCommerce added
209					paymentBox.innerHTML = '';
210					paymentBox.id = 'flow-container';
211					paymentBox.classList.add('cko-flow__container');
212					paymentBox.style.padding = '0';
213					
214					// Move Flow content into the new payment_box
215					while (detachedFlowContainer.firstChild) {
216						paymentBox.appendChild(detachedFlowContainer.firstChild);
217					}
218					
219					if (typeof ckoLogger !== 'undefined') {
220						ckoLogger.debug('[COUPON GUARD] ✅ Flow content moved to new payment_box (WooCommerce replaced DOM)');
221					}
222				} else {
223					if (typeof ckoLogger !== 'undefined') {
224						ckoLogger.error('[COUPON GUARD] ❌ Cannot reattach: payment_box not found');
225					}
226				}
227			} else {
228				if (typeof ckoLogger !== 'undefined') {
229					ckoLogger.error('[COUPON GUARD] ❌ Cannot reattach: payment method container not found');
230				}
231			}
232		}
233
234		// Clear reference
235		detachedFlowContainer = null;
236		return true;
237	}
238
239	/**
240	 * Intercept coupon application to track amount changes.
241	 * 
242	 * NOTE: When preserve_card_on_update is enabled, WooCommerce no longer replaces 
243	 * the payment methods section due to PHP filter (exclude_payment_method_from_fragments). 
244	 * This interceptor tracks the coupon action so we can update the pending amount for handleSubmit.
245	 * When disabled, Flow will reload normally after coupon apply.
246	 */
247	function attachCouponInterceptor() {
248		if (typeof jQuery === 'undefined') {
249			return;
250		}
251
252		// Helper function to handle coupon action
253		function handleCouponAction(actionName) {
254			// Only set flag if preserve card feature is enabled
255			if (!preserveCardEnabled) {
256				if (typeof ckoLogger !== 'undefined') {
257					ckoLogger.debug('[COUPON] Preserve card feature disabled - Flow will reload after ' + actionName);
258				}
259				return;
260			}
261
262			// Check if Flow is initialized
263			if (typeof ckoFlow !== 'undefined' && ckoFlow.flowComponent && 
264			    typeof FlowState !== 'undefined' && FlowState.get('initialized')) {
265				
266				window.ckoFlowAmountUpdateInProgress = true;
267				if (typeof ckoLogger !== 'undefined') {
268					ckoLogger.debug('[COUPON] 🛡️ ' + actionName + ' - marking amount update in progress');
269				}
270			}
271		}
272
273		// Intercept coupon form submission
274		jQuery(document).on('click', '.woocommerce-form-coupon .button, [name="apply_coupon"]', function() {
275			handleCouponAction('Coupon apply clicked');
276		});
277
278		// Also intercept coupon removal
279		jQuery(document).on('click', '.woocommerce-remove-coupon', function() {
280			handleCouponAction('Coupon remove clicked');
281		});
282
283		// Intercept Enter key in coupon field
284		jQuery(document).on('keypress', '#coupon_code', function(e) {
285			if (e.which === 13) { // Enter key
286				handleCouponAction('Enter pressed in coupon field');
287			}
288		});
289		
290		// Listen for WooCommerce AJAX completion to update pending amount
291		// The payment methods section is preserved (not replaced) by PHP filter,
292		// so we just need to update the amount for handleSubmit
293		jQuery(document).ajaxComplete(function(event, xhr, settings) {
294			// Only handle WooCommerce checkout AJAX requests
295			if (!settings.url || settings.url.indexOf('wc-ajax') === -1) {
296				return;
297			}
298			
299			// Check if this is a checkout update or coupon action
300			const isCouponAction = settings.url.indexOf('apply_coupon') !== -1 || 
301			                       settings.url.indexOf('remove_coupon') !== -1 ||
302			                       settings.url.indexOf('update_order_review') !== -1;
303			
304			if (!isCouponAction) {
305				return;
306			}
307			
308		// Update pending amount and items after coupon operations
309		if (window.ckoFlowAmountUpdateInProgress) {
310			// Wait for WooCommerce to finish updating the DOM with new totals
311			setTimeout(function() {
312				const currentOrderAmount = getCurrentOrderTotalFromDOM();
313				
314				if (currentOrderAmount && typeof ckoFlow !== 'undefined') {
315					// Check if current payment type is an APM that doesn't support amount updates
316					// Only Card, Apple Pay, and Google Pay support dynamic amount adjustment
317					const paymentType = ckoFlow.selectedPaymentType?.toLowerCase() || '';
318					const typesWithAmountSupport = ['card', 'applepay', 'googlepay'];
319					const isAPMWithoutSupport = paymentType && !typesWithAmountSupport.includes(paymentType);
320					const amountChanged = ckoFlow.initialSessionAmount !== null && 
321						currentOrderAmount !== ckoFlow.initialSessionAmount;
322					
323					if (isAPMWithoutSupport && amountChanged) {
324						// APM selected that doesn't support amount updates - trigger reload
325						if (typeof ckoLogger !== 'undefined') {
326							ckoLogger.debug('[COUPON GUARD] APM detected without amount support:', paymentType);
327							ckoLogger.debug('[COUPON GUARD] Triggering Flow reload for amount change');
328						}
329						
330						// Don't set pending amount - we're reloading
331						ckoFlow.pendingAmountUpdate = null;
332						
333						// Trigger Flow reload
334						setTimeout(function() {
335							if (typeof reloadFlowComponent === 'function') {
336								reloadFlowComponent();
337							} else if (typeof window.reloadFlowComponent === 'function') {
338								window.reloadFlowComponent();
339							}
340						}, 100);
341					} else {
342						// Card/Apple Pay/Google Pay or no payment selected - use pending amount
343						ckoFlow.pendingAmountUpdate = currentOrderAmount;
344						if (typeof ckoLogger !== 'undefined') {
345							ckoLogger.debug('[COUPON GUARD] ✅ Updated pendingAmountUpdate after coupon action:', currentOrderAmount);
346						}
347						
348						// CRITICAL: For Apple Pay and Google Pay, also call checkout.update() 
349						// to update the payment sheet amount shown to the customer
350						if ((paymentType === 'applepay' || paymentType === 'googlepay') && 
351							ckoFlow.checkoutInstance && 
352							typeof ckoFlow.checkoutInstance.update === 'function') {
353							ckoFlow.checkoutInstance.update({ amount: currentOrderAmount })
354								.then(function() {
355									if (typeof ckoLogger !== 'undefined') {
356										ckoLogger.debug('[COUPON GUARD] ✅ checkout.update() succeeded for', paymentType);
357									}
358								})
359								.catch(function(error) {
360									if (typeof ckoLogger !== 'undefined') {
361										ckoLogger.debug('[COUPON GUARD] checkout.update() failed:', error?.message || 'unknown');
362									}
363								});
364						}
365					}
366				}
367				
368				// Clear flag
369				window.ckoFlowAmountUpdateInProgress = false;
370				if (typeof ckoLogger !== 'undefined') {
371					ckoLogger.debug('[COUPON GUARD] ✅ Amount update complete - flag cleared');
372				}
373			}, 500); // Increased delay to ensure WooCommerce DOM updates are complete
374		}
375		});
376	}
377	
378	/**
379	 * Handle reattachment after WooCommerce finishes DOM updates.
380	 * Called from the main updated_checkout handler.
381	 * Only operates when preserve_card_on_update is enabled.
382	 */
383	function handleReattachmentIfNeeded() {
384		// Skip if feature is disabled
385		if (!preserveCardEnabled) {
386			return false;
387		}
388
389		if (typeof ckoLogger !== 'undefined') {
390			ckoLogger.debug('[COUPON GUARD] handleReattachmentIfNeeded called', {
391				amountUpdateInProgress: window.ckoFlowAmountUpdateInProgress,
392				hasDetachedContainer: !!detachedFlowContainer
393			});
394		}
395		
396		if (!window.ckoFlowAmountUpdateInProgress) {
397			return false;
398		}
399		
400		if (!detachedFlowContainer) {
401			if (typeof ckoLogger !== 'undefined') {
402				ckoLogger.debug('[COUPON GUARD] Amount update in progress but no detached container - skipping reattach');
403			}
404			return false;
405		}
406		
407		if (typeof ckoLogger !== 'undefined') {
408			ckoLogger.debug('[COUPON GUARD] 🔄 Starting reattachment process...');
409		}
410		
411		// Wait a tick for WooCommerce to finish DOM updates
412		setTimeout(function() {
413			const success = reattachFlowContainer();
414			
415			if (success) {
416				// Update pending amount for handleSubmit
417				const currentOrderAmount = getCurrentOrderTotalFromDOM();
418				
419				if (currentOrderAmount && typeof ckoFlow !== 'undefined') {
420					// Check if current payment type is an APM that doesn't support amount updates
421					const paymentType = ckoFlow.selectedPaymentType?.toLowerCase() || '';
422					const typesWithAmountSupport = ['card', 'applepay', 'googlepay'];
423					const isAPMWithoutSupport = paymentType && !typesWithAmountSupport.includes(paymentType);
424					const amountChanged = ckoFlow.initialSessionAmount !== null && 
425						currentOrderAmount !== ckoFlow.initialSessionAmount;
426					
427					if (isAPMWithoutSupport && amountChanged) {
428						// APM selected - trigger reload instead of setting pending amount
429						if (typeof ckoLogger !== 'undefined') {
430							ckoLogger.debug('[COUPON GUARD] APM detected in reattachment - triggering reload');
431						}
432						ckoFlow.pendingAmountUpdate = null;
433						setTimeout(function() {
434							if (typeof reloadFlowComponent === 'function') {
435								reloadFlowComponent();
436							} else if (typeof window.reloadFlowComponent === 'function') {
437								window.reloadFlowComponent();
438							}
439						}, 100);
440					} else {
441						ckoFlow.pendingAmountUpdate = currentOrderAmount;
442						if (typeof ckoLogger !== 'undefined') {
443							ckoLogger.debug('[COUPON GUARD] Updated pendingAmountUpdate:', currentOrderAmount);
444						}
445						
446						// CRITICAL: For Apple Pay and Google Pay, also call checkout.update(
446)
447						if ((paymentType === 'applepay' || paymentType === 'googlepay') && 
448							ckoFlow.checkoutInstance && 
449							typeof ckoFlow.checkoutInstance.update === 'function') {
450							ckoFlow.checkoutInstance.update({ amount: currentOrderAmount })
451								.then(function() {
452									if (typeof ckoLogger !== 'undefined') {
453										ckoLogger.debug('[COUPON GUARD] ✅ checkout.update() succeeded for', paymentType);
454									}
455								})
456								.catch(function(error) {
457									if (typeof ckoLogger !== 'undefined') {
458										ckoLogger.debug('[COUPON GUARD] checkout.update() failed:', error?.message || 'unknown');
459									}
460								});
461						}
462					}
463				}
464			}
465			
466			// Clear flag after reattachment attempt (success or fail)
467			setTimeout(function() {
468				window.ckoFlowAmountUpdateInProgress = false;
469				if (typeof ckoLogger !== 'undefined') {
470					ckoLogger.debug('[COUPON GUARD] ✅ Amount update complete - flag cleared');
471				}
472			}, 500);
473		}, 150);
474		
475		return true;
476	}
477
478	function attachHandler() {
479		if (typeof jQuery === 'undefined') {
480			return;
481		}
482
483		// Attach coupon interceptor
484		attachCouponInterceptor();
485
486		// Remove any existing handler first
487		jQuery(document).off('updated_checkout.cko-terms-prevention');
488		jQuery(document.body).off('updated_checkout.cko-flow-guard');
489
490		// Attach handler to BOTH body and document.body to ensure we catch the event
491		// WooCommerce fires updated_checkout on $(document.body)
492		jQuery(document.body).on('updated_checkout.cko-flow-guard', function (event) {
493			// Track updated_checkout events to detect multiple reloads
494			if (!window.ckoUpdatedCheckoutCount) {
495				window.ckoUpdatedCheckoutCount = 0;
496				window.ckoUpdatedCheckoutTimestamps = [];
497			}
498			window.ckoUpdatedCheckoutCount++;
499			const now = Date.now();
500			window.ckoUpdatedCheckoutTimestamps.push(now);
501
502			// Keep only last 10 timestamps
503			if (window.ckoUpdatedCheckoutTimestamps.length > 10) {
504				window.ckoUpdatedCheckoutTimestamps.shift();
505			}
506
507			const debugEnabled = typeof ckoLogger !== 'undefined' && ckoLogger.debugEnabled;
508
509			// Check for rapid-fire events (multiple within 500ms)
510			const recentEvents = window.ckoUpdatedCheckoutTimestamps.filter((ts) => now - ts < 500);
511			if (recentEvents.length > 1 && debugEnabled) {
512				ckoLogger.warn(
513					`⚠️ MULTIPLE updated_checkout events detected: ${recentEvents.length} events in last 500ms (Total: ${window.ckoUpdatedCheckoutCount})`
514				);
515			}
516
517			// Log in debug mode only to avoid noisy logs on hot path
518			if (debugEnabled) {
519				ckoLogger.debug(
520					`updated_checkout EVENT #${window.ckoUpdatedCheckoutCount} fired at ${new Date().toLocaleTimeString()}`
521				);
522				ckoLogger.debug(`===== updated_checkout EVENT FIRED (#${window.ckoUpdatedCheckoutCount}) =====`);
523			}
524			
525			// CRITICAL: Handle Flow container reattachment if a coupon operation detached it
526			// This must happen FIRST, before any other updated_checkout logic
527			if (handleReattachmentIfNeeded()) {
528				// Flow was detached and is being reattached - skip all other processing
529				// The reattachment handler will update the pending amount
530				if (debugEnabled) {
531					ckoLogger.debug('[UPDATED_CHECKOUT] Flow reattachment in progress - skipping other handlers');
532				}
533				return;
534			}
535
536			// CRITICAL: Prevent update_checkout if it was triggered by a terms checkbox
537			// Check prevention flag first (set by terms-prevention module)
538			// Also check if the last clicked/changed element was a terms checkbox (backup check)
539			const activeElement = document.activeElement;
540			const lastClicked = window.ckoTermsCheckboxLastClicked;
541			const timeSinceLastClick = Date.now() - window.ckoTermsCheckboxLastClickTime;
542			const hasTermsHelper = typeof window.isTermsCheckbox === 'function';
543			const isTermsTriggered =
544				window.ckoPreventUpdateCheckout ||
545				(hasTermsHelper && lastClicked && window.isTermsCheckbox(lastClicked) && timeSinceLastClick < 2000) ||
546				(hasTermsHelper && activeElement && window.isTermsCheckbox(activeElement));
547
548			if (isTermsTriggered) {
549				ckoLogger.debug('🚫 updated_checkout triggered by terms checkbox - skipping Flow-specific handling only', {
550					preventionFlag: window.ckoPreventUpdateCheckout,
551					lastClickedId: lastClicked ? lastClicked.id || 'no-id' : 'none',
552					activeElementId: activeElement ? activeElement.id || 'no-id' : 'none',
553					timeSinceClick: timeSinceLastClick + 'ms'
554				});
555
556				// Clear tracking after a delay
557				setTimeout(function () {
558					window.ckoPreventUpdateCheckout = false;
559					window.ckoTermsCheckboxLastClicked = null;
560				}, 3000);
561
562				// NOTE: We no longer block the event propagation - this was interfering with 
563				// WooCommerce's normal checkout flow and preventing the loading overlay from being removed.
564				// We just skip our Flow-specific handling and let WooCommerce continue normally.
565				return;
566			}
567
568			// Log field values BEFORE DOM update
569			const emailBefore = getCheckoutFieldValue('billing_email');
570			const addressBefore = getCheckoutFieldValue('billing_address_1');
571			const cityBefore = getCheckoutFieldValue('billing_city');
572			const countryBefore = getCheckoutFieldValue('billing_country');
573			const flowPaymentBefore = document.getElementById('payment_method_wc_checkout_com_flow');
574
575			ckoLogger.debug('State BEFORE updated_checkout:', {
576				email: emailBefore || 'EMPTY',
577				address1: addressBefore || 'EMPTY',
578				city: cityBefore || 'EMPTY',
579				country: countryBefore || 'EMPTY',
580				flowPaymentChecked: flowPaymentBefore?.checked || false,
581				flowInitialized: FlowState.get('initialized')
582			});
583
584			// CRITICAL: Skip if we're handling a 3DS return (don't re-initialize Flow)
585			// Check both the flag and URL parameters
586			if (window.ckoFlow3DSReturn) {
587				ckoLogger.threeDS('Skipping updated_checkout handler - 3DS return flag is set');
588				return;
589			}
590
591			// Also check URL parameters as fallback
592			const urlParams = new URLSearchParams(window.location.search);
593			const paymentId = urlParams.get('cko-payment-id');
594			const sessionId = urlParams.get('cko-session-id');
595			const paymentSessionId = urlParams.get('cko-payment-session-id');
596
597			if (paymentId || sessionId || paymentSessionId) {
598				ckoLogger.threeDS('Skipping updated_checkout handler - 3DS return detected in URL');
599				FlowState.set('is3DSReturn', true);
600				return;
601			}
602
603			// Check if cart total changed (e.g. coupon applied/removed).
604			// Use helper function to read the most up-to-date value from DOM.
605			// order_amount is in cents (minor units).
606			const currentOrderAmount = getCurrentOrderTotalFromDOM();
607
608			if (currentOrderAmount !== null && previousOrderAmount !== null && currentOrderAmount !== previousOrderAmount) {
609				if (typeof ckoLogger !== 'undefined') {
610					ckoLogger.debug('[CART CHANGE] Cart total changed (e.g. coupon applied/removed)', {
611						previous: previousOrderAmount,
612						current: currentOrderAmount
613					});
614				}
615
616				previousOrderAmount = currentOrderAmount;
617
618				// CRITICAL: Clear existing order data when cart changes.
619				// This ensures a NEW order is created with the correct amount.
620				// Prevents reusing an order created with the old (wrong) amount.
621				const orderIdField = jQuery('input[name="order_id"]');
622				if (orderIdField.length && orderIdField.val()) {
623					const oldOrderId = orderIdField.val();
624					if (typeof ckoLogger !== 'undefined') {
625						ckoLogger.debug('[CART CHANGE] Clearing stale order_id from form: ' + oldOrderId + ' (will create new order with correct amount)');
626					}
627					orderIdField.val('');
628				}
629
630				// Also clear from session storage
631				if (typeof FlowSessionStorage !== 'undefined') {
632					const sessionOrderId = FlowSessionStorage.getOrderId();
633					if (sessionOrderId) {
634						if (typeof ckoLogger !== 'undefined') {
635							ckoLogger.debug('[CART CHANGE] Clearing stale order_id from session storage: ' + sessionOrderId);
636						}
637						FlowSessionStorage.clearOrderData();
638					}
639				}
640
641				// DYNAMIC AMOUNT ADJUSTMENT: Instead of reloading Flow component,
642				// store the new amount for handleSubmit to use when payment is submitted.
643				// This preserves card details and avoids component reload.
644				// Works for card, Apple Pay, and Google Pay payments.
645				
646				// CRITICAL FIX: Set flag to prevent flow-container-ready-handler from remounting
647				// This flag tells other modules that we're handling a cart change with dynamic amount update
648				// so they should NOT reinitialize Flow (which would destroy card details)
649				if (typeof ckoFlow !== 'undefined' && ckoFlow.checkoutInstance) {
650					window.ckoFlowAmountUpdateInProgress = true;
651					if (typeof ckoLogger !== 'undefined') {
652						ckoLogger.debug('[CART CHANGE] 🛡️ Setting ckoFlowAmountUpdateInProgress flag to prevent Flow reload');
653					}
654				}
655
656				setTimeout(function () {
657					if (typeof FlowState !== 'undefined' && FlowState.get('initialized') &&
658						typeof ckoFlow !== 'undefined' && ckoFlow.flowComponent) {
659						
660						// Check if checkout instance supports dynamic updates
661						if (ckoFlow.checkoutInstance && typeof ckoFlow.checkoutInstance.update === 'function') {
662							if (typeof ckoLogger !== 'undefined') {
663								ckoLogger.debug('[CART CHANGE] 🚀 Using dynamic amount update (no reload needed)', {
664									previousAmount: ckoFlow.initialSessionAmount,
665									newAmount: currentOrderAmount,
666									hasCheckoutInstance: true
667								});
668							}
669
670							// Store pending amount for handleSubmit callback
671							ckoFlow.pendingAmountUpdate = currentOrderAmount;
672
673							// Try to update payment sheet for Apple Pay/Google Pay (client-side update)
674							// This updates the visual amount shown in wallet payment sheets
675							try {
676								ckoFlow.checkoutInstance.update({
677									amount: currentOrderAmount
678								}).then(function() {
679									if (typeof ckoLogger !== 'undefined') {
680										ckoLogger.debug('[CART CHANGE] ✅ checkout.update() succeeded - payment sheet updated');
681									}
682								}).catch(function(error) {
683									// checkout.update() may not be supported for all payment types
684									// That's OK - handleSubmit will still use the updated amount
685									if (typeof ckoLogger !== 'undefined') {
686										ckoLogger.debug('[CART CHANGE] checkout.update() not applicable (expected for card payments)', {
687											error: error?.message || 'unknown'
688										});
689									}
690								});
691							} catch (error) {
692								// Silently handle - checkout.update is primarily for wallet payment sheets
693								if (typeof ckoLogger !== 'undefined') {
694									ckoLogger.debug('[CART CHANGE] checkout.update() threw error (expected for some payment types)', {
695										error: error?.message || 'unknown'
696									});
697								}
698							}
699
700							// Clear the flag after a delay to allow container-ready handler to skip
701							setTimeout(function() {
702								window.ckoFlowAmountUpdateInProgress = false;
703								if (typeof ckoLogger !== 'undefined') {
704									ckoLogger.debug('[CART CHANGE] 🛡️ Cleared ckoFlowAmountUpdateInProgress flag');
705								}
706							}, 2000);
707							
708						} else if (typeof canInitializeFlow === 'function' && canInitializeFlow()) {
709							// Fallback: If checkout instance not available, use old reload behavior
710							window.ckoFlowAmountUpdateInProgress = false;
711							if (typeof reloadFlowComponent === 'function') {
712								if (typeof ckoLogger !== 'undefined') {
713									ckoLogger.debug('[CART CHANGE] Fallback: Reloading Flow component (checkoutInstance not available)');
714								}
715								reloadFlowComponent();
716							}
717						}
718					} else {
719						// Flow not initialized, clear the flag
720						window.ckoFlowAmountUpdateInProgress = false;
721					}
722				}, 300);
723			} else if (currentOrderAmount !== null) {
724				previousOrderAmount = currentOrderAmount;
725			}
726
727			ckoLogger.debug('updated_checkout event fired');
728			ckoLogger.debug('Flow state BEFORE updated_checkout:', {
729				flowInitialized: FlowState.get('initialized'),
730				flowComponentExists: !!ckoFlow.flowComponent,
731				flowComponentRootExists: !!document.querySelector('[data-testid="checkout-web-component-root"]'),
732				flowContainerExists: !!document.getElementById('flow-container'),
733				flowPaymentChecked: document.getElementById('payment_method_wc_checkout_com_flow')?.checked || false
734			});
735
736			// EVENT-DRIVEN: Flow remounting is now handled by cko:flow-container-ready event listener
737			// No need for setTimeout delays - flow-container.js will emit event when container is ready
738			
739			// CRITICAL: Remove WooCommerce loading overlay after updated_checkout completes
740			// This ensures the checkout is interactive after coupon operations
741			// Use a small delay to allow WooCommerce to finish its AJAX response handling
742			setTimeout(function() {
743				if (typeof jQuery !== 'undefined') {
744					jQuery('.woocommerce').removeClass('processing').unblock();
745					jQuery('.woocommerce-checkout').removeClass('processing').unblock();
746					jQuery('form.checkout').removeClass('processing').unblock();
747					jQuery('.blockOverlay').remove();
748					ckoLogger.debug('[UPDATED_CHECKOUT] ✓ WooCommerce loading overlay removed after update');
749				}
750			}, 500);
751		});
752	}
753
754	// Global overlay removal function - can be called from anywhere
755	function removeWooCommerceOverlay() {
756		if (typeof jQuery !== 'undefined') {
757			jQuery('.woocommerce').removeClass('processing').unblock();
758			jQuery('.woocommerce-checkout').removeClass('processing').unblock();
759			jQuery('form.checkout').removeClass('processing').unblock();
760			jQuery('.blockOverlay').remove();
761		}
762	}
763
764	// Setup global AJAX complete handler to always remove overlay after checkout AJAX
765	function setupAjaxOverlayHandler() {
766		if (typeof jQuery === 'undefined') return;
767		
768		jQuery(document).ajaxComplete(function(event, xhr, settings) {
769			// Only handle WooCommerce checkout related AJAX
770			if (settings && settings.url && 
771				(settings.url.indexOf('wc-ajax') !== -1 || 
772				 settings.url.indexOf('update_order_review') !== -1 ||
773				 settings.url.indexOf('apply_coupon') !== -1 ||
774				 settings.url.indexOf('remove_coupon') !== -1)) {
775				
776				ckoLogger.debug('[AJAX COMPLETE] Checkout AJAX finished, removing overlay', {
777					url: settings.url
778				});
779				
780				// Remove overlay after a small delay to let WooCommerce finish processing
781				setTimeout(removeWooCommerceOverlay, 100);
782				// And again after a longer delay as backup
783				setTimeout(removeWooCommerceOverlay, 500);
784			}
785		});
786		
787		ckoLogger.debug('[OVERLAY GUARD] AJAX complete handler attached');
788	}
789
790	/**
791	 * Setup AJAX prefilter to add cko_flow_initialized flag to WooCommerce checkout AJAX requests.
792	 * This tells the PHP fragment filter that Flow is already initialized and it's safe to
793	 * exclude the payment methods fragment from updates.
794	 */
795	function setupAjaxPrefilter() {
796		if (typeof jQuery === 'undefined') return;
797		
798		jQuery.ajaxPrefilter(function(options, originalOptions, jqXHR) {
799			// Only modify WooCommerce checkout-related AJAX requests
800			if (!options.url || options.url.indexOf('wc-ajax') === -1) {
801				return;
802			}
803			
804			// Only add flag if Flow is initialized
805			if (typeof FlowState === 'undefined' || !FlowState.get('initialized')) {
806				return;
807			}
808			
809			// Add the flag to the request data
810			if (options.type && options.type.toUpperCase() === 'POST') {
811				if (typeof options.data === 'string') {
812					// URL-encoded data
813					options.data += '&cko_flow_initialized=1';
814				} else if (typeof options.data === 'object' && options.data !== null) {
815					// Object data
816					options.data.cko_flow_initialized = '1';
817				}
818				
819				if (typeof ckoLogger !== 'undefined') {
820					ckoLogger.debug('[AJAX PREFILTER] Added cko_flow_initialized flag to WooCommerce AJAX request');
821				}
822			}
823		});
824		
825		if (typeof ckoLogger !== 'undefined') {
826			ckoLogger.debug('[AJAX PREFILTER] WooCommerce AJAX prefilter registered');
827		}
828	}
829
830	window.FlowUpdatedCheckoutGuard = {
831		init: function () {
832			if (initialized) {
833				return;
834			}
835			initialized = true;
836			attachHandler();
837			setupAjaxOverlayHandler();
838			setupAjaxPrefilter();
839		},
840		removeOverlay: removeWooCommerceOverlay,
841		getCurrentOrderTotalFromDOM: getCurrentOrderTotalFromDOM
842	};
843})();

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.