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.