1(function ($) { 2 /** 3 * The classic checkout nonce field, as it appears in a serialized form payload. 4 * 5 * Shared so the "does this submit carry one?" test and the replacement use exactly 6 * the same definition. Not a literal in a loop, so the lastIndex statefulness of a 7 * /g regex cannot bite here. 8 */ 9 const CHECKOUT_NONCE_FIELD = /(^|&)woocommerce-process-checkout-nonce=[^&]*/; 10 11 class PPCPManager { 12 constructor(ppcp_manager) { 13 // 9.0.66 14 this.ppcp_manager = ppcp_manager; 15 this.productAddToCart = true; 16 this.lastApiResponse = null; 17 this.ppcp_address = []; 18 this.paymentsClient = null; 19 this.allowedPaymentMethods = []; 20 this.merchantInfo = null; 21 this.pageContext = 'unknown'; 22 this.ppcp_used_payment_method = null; 23 this.googleSelectedShippingId = ''; 24 this.googleShippingIdMap = {}; 25 this.appleSelectedShippingId = ''; 26 this.appleShippingIdMap = {}; 27 // How long the card-submit latch may stay set before a further Place order 28 // click is allowed through. Long enough to cover a real 3-D Secure challenge 29 // (a banking-app approval can take a minute or two), short enough that a 30 // buyer whose challenge window was lost is not stuck on a dead button. 31 this.ccSubmitLatchTimeout = 3 * 60 * 1000; 32 // The CardFields instance that owns the currently mounted hosted fields. 33 this.cardFieldsInstance = null; 34 this.init(); 35 this.ppcp_cart_css(); 36 } 37 38 init() { 39 if (typeof this.ppcp_manager === 'undefined') { 40 console.log("PPCP Manager configuration is undefined."); 41 return false; 42 } 43 const lastError = (ppcp_manager?.last_error || '').trim(); 44 if (lastError) { 45 console.log(lastError); 46 $(document.body).trigger('ppcp_checkout_error', lastError); 47 } 48 this.debouncedTogglePlaceOrderButton = this.debounce_place_order(this.togglePlaceOrderButton.bind(this), 4); 49 this.manageVariations('#ppcp_product, .google-pay-container, .apple-pay-container'); 50 this.manageSubscriptionOptions(); 51 this.prefetchProductTotal(); 52 this.bindCheckoutEvents(); 53 if (this.ppcp_manager.advanced_card_payments === 'yes') { 54 this.debouncedUpdatePaypalCC = this.debounce_cc(this.syncPayPalCardFields.bind(this), 500); 55 } 56 this.debouncedUpdatePaypalCheckout = this.debounce(this.syncPayPalCheckoutButtons.bind(this), 500); 57 if (this.ppcp_manager.enabled_google_pay === 'yes') { 58 this.debouncedUpdateGooglePay = this.debounce_google(this.syncGooglePayButton.bind(this), 500); 59 } 60 if (this.ppcp_manager.enabled_apple_pay === 'yes') { 61 this.debouncedUpdateApplePay = this.debounce_apple(this.syncApplePayButton.bind(this), 500); 62 } 63 if (this.isCheckoutPage() === false) { 64 this.debouncedUpdatePaypalCheckout(); 65 if (this.ppcp_manager.enabled_google_pay === 'yes') { 66 this.debouncedUpdateGooglePay(); 67 } 68 if (this.ppcp_manager.enabled_apple_pay === 'yes') { 69 this.debouncedUpdateApplePay(); 70 } 71 } else { 72 this.debouncedUpdatePaypalCheckout(); 73 if (this.ppcp_manager.enabled_google_pay === 'yes') { 74 this.debouncedUpdateGooglePay(); 75 } 76 if (this.ppcp_manager.enabled_apple_pay === 'yes') { 77 this.debouncedUpdateApplePay(); 78 } 79 if (this.ppcp_manager.advanced_card_payments === 'yes') { 80 this.debouncedUpdatePaypalCC(); 81 } 82 } 83 setTimeout(function () { 84 $('#wfacp_smart_buttons.wfacp-dynamic-checkout-loading .dynamic-checkout__skeleton').hide(); 85 }, 1000); 86 } 87 88 t(key, fallback = '') { 89 const raw = this.ppcp_manager ? this.ppcp_manager[key] : undefined; 90 if (raw === null || raw === undefined || raw === '') { 91 return fallback; 92 } 93 const v = String(raw); 94 return (v && v !== 'undefined' && v !== 'null') ? v : fallback; 95 } 96 97 getAddress(prefix) { 98 const fields = { 99 addressLine1: jQuery(`#${prefix}_address_1`).val(), 100 addressLine2: jQuery(`#${prefix}_address_2`).val(), 101 adminArea1: jQuery(`#${prefix}_state`).val(), 102 adminArea2: jQuery(`#${prefix}_city`).val(), 103 postalCode: jQuery(`#${prefix}_postcode`).val(), 104 countryCode: jQuery(`#${prefix}_country`).val(), 105 firstName: jQuery(`#${prefix}_first_name`).val(), 106 lastName: jQuery(`#${prefix}_last_name`).val(), 107 email: jQuery(`#${prefix}_email`).val() 108 }; 109 110 fields.phoneNumber = prefix === 'billing' ? 111 jQuery('#billing-phone').val() || jQuery('#shipping-phone').val() : 112 jQuery('#shipping-phone').val() || jQuery('#billing-phone').val(); 113 114 let customerData = {}; 115 let addressData = {}; 116 117 if (!fields.addressLine1) { 118 if (typeof wp !== 'undefined' && wp.data?.select) { 119 try { 120 customerData = wp.data.select('wc/store/cart').getCustomerData(); 121 } catch (e) { 122 console.warn('Could not fetch customerData:', e); 123 } 124 } 125 const {billingAddress, shippingAddress} = customerData; 126 addressData = (prefix === 'billing') ? billingAddress : shippingAddress; 127 128 Object.assign(fields, { 129 addressLine1: addressData.address_1, 130 addressLine2: addressData.address_2, 131 adminArea1: addressData.state, 132 adminArea2: addressData.city, 133 postalCode: addressData.postcode, 134 countryCode: addressData.country, 135 firstName: addressData.first_name, 136 lastName: addressData.last_name, 137 email: prefix === 'billing' ? billingAddress.email || shippingAddress.email : shippingAddress.email || billingAddress.email 138 }); 139 } 140 141 // Start with the standard fields 142 const result = { 143 [`${prefix}_address_1`]: fields.addressLine1 || '', 144 [`${prefix}_address_2`]: fields.addressLine2 || '', 145 [`${prefix}_state`]: fields.adminArea1 || '', 146 [`${prefix}_city`]: fields.adminArea2 || '', 147 [`${prefix}_postcode`]: fields.postalCode || '', 148 [`${prefix}_country`]: fields.countryCode || '', 149 [`${prefix}_first_name`]: fields.firstName || '', 150 [`${prefix}_last_name`]: fields.lastName || '', 151 [`${prefix}_email`]: fields.email || '', 152 [`${prefix}_phone`]: fields.phoneNumber || '' 153 }; 154 155 // Add ALL other fields from addressData (including custom fields) 156 if (addressData && Object.keys(addressData).length > 0) {
157 Object.keys(addressData).forEach(key => { 158 const fieldKey = `${prefix}_${key}`; 159 // Only add if it's not already in our standard fields 160 if (!result.hasOwnProperty(fieldKey)) { 161 result[fieldKey] = addressData[key] !== undefined && addressData[key] !== null 162 ? addressData[key] 163 : ''; 164 } 165 }); 166 } 167 168 return result; 169 } 170 171 getValidAddress(prefix) { 172 const address = this.getAddress(prefix); 173 if (this.isValidAddress(prefix, address)) { 174 return address; 175 } 176 // Fall back to the other address (e.g. when "Use same address for billing" 177 // is checked on Block Checkout the billing address is empty in the store). 178 // Re-key the fallback to the requested prefix, otherwise it is posted with 179 // the wrong field names (shipping_* instead of billing_*) and the resulting 180 // WooCommerce order is created without a billing address. 181 const otherPrefix = prefix === 'billing' ? 'shipping' : 'billing'; 182 const otherAddress = this.getAddress(otherPrefix); 183 if (this.isValidAddress(otherPrefix, otherAddress)) { 184 return this.rekeyAddress(otherAddress, otherPrefix, prefix); 185 } 186 return address; 187 } 188 189 // Re-map an address object's field keys from one prefix to another, e.g. 190 // { shipping_address_1: '...' } -> { billing_address_1: '...' }. 191 rekeyAddress(address, fromPrefix, toPrefix) { 192 if (!address) { 193 return address; 194 } 195 const result = {}; 196 const search = `${fromPrefix}_`; 197 Object.keys(address).forEach(key => { 198 if (key.indexOf(search) === 0) { 199 result[`${toPrefix}_${key.substring(search.length)}`] = address[key]; 200 } else { 201 result[key] = address[key]; 202 } 203 }); 204 return result; 205 } 206 207 getBillingAddress() { 208 return this.getValidAddress('billing'); 209 } 210 211 getShippingAddress() { 212 return this.getValidAddress('shipping'); 213 } 214 215 isValidAddress(prefix, address) { 216 return address && address[`${prefix}_address_1`]; 217 } 218 219 // Convert a Google Pay / Apple Pay wallet address to WooCommerce form-field format. 220 // Used as a fallback when the Block Checkout form has not been filled in 221 // before the customer clicks the express wallet button. Apple Pay populates 222 // both `name` (given) and `surname` (family); Google Pay puts the full name 223 // in `name` only. 224 walletToWCFormat(walletAddr, prefix) { 225 if (!walletAddr) { 226 return {}; 227 } 228 const rawName = (walletAddr.name || '').trim(); 229 const rawSurname = (walletAddr.surname || '').trim(); 230 let firstName = ''; 231 let lastName = ''; 232 if (rawName && rawSurname) { 233 firstName = rawName; 234 lastName = rawSurname; 235 } else if (rawName) { 236 const parts = rawName.split(/\s+/); 237 firstName = parts.shift() || ''; 238 lastName = parts.join(' '); 239 } else if (rawSurname) { 240 lastName = rawSurname; 241 } 242 const result = { 243 [`${prefix}_first_name`]: firstName, 244 [`${prefix}_last_name`]: lastName, 245 [`${prefix}_address_1`]: walletAddr.address1 || '', 246 [`${prefix}_address_2`]: walletAddr.address2 || '', 247 [`${prefix}_city`]: walletAddr.city || '', 248 [`${prefix}_state`]: walletAddr.state || '', 249 [`${prefix}_postcode`]: walletAddr.postcode || '', 250 [`${prefix}_country`]: walletAddr.country || '', 251 [`${prefix}_phone`]: walletAddr.phoneNumber || '' 252 }; 253 if (prefix === 'billing' && walletAddr.emailAddress) { 254 result.billing_email = walletAddr.emailAddress; 255 } 256 return result; 257 } 258 259 isCheckoutPage() { 260 return this.ppcp_manager.page === 'checkout'; 261 } 262 263 isProductPage() { 264 return this.ppcp_manager.page === 'product'; 265 } 266 267 isCartPage() { 268 return this.ppcp_manager.page === 'cart'; 269 } 270 271 isSale() { 272 return this.ppcp_manager.paymentaction === 'capture'; 273 } 274 275 throttle(func, limit) { 276 let lastCall = 0; 277 return function (...args) { 278 const now = Date.now(); 279 if (now - lastCall >= limit) {
280 lastCall = now; 281 func.apply(this, args); 282 } 283 }; 284 } 285 286 debounce(func, delay) { 287 let timer; 288 return function (...args) { 289 clearTimeout(timer); 290 timer = setTimeout(() => func.apply(this, args), delay); 291 }; 292 } 293 294 debounce_google(func, delay) { 295 let timer; 296 return function (...args) { 297 clearTimeout(timer); 298 timer = setTimeout(() => func.apply(this, args), delay); 299 }; 300 } 301 302 debounce_apple(func, delay) { 303 let timer; 304 return function (...args) { 305 clearTimeout(timer); 306 timer = setTimeout(() => func.apply(this, args), delay); 307 }; 308 } 309 310 debounce_place_order(func, delay) { 311 let timer; 312 return function (...args) { 313 clearTimeout(timer); 314 timer = setTimeout(() => func.apply(this, args), delay); 315 }; 316 } 317 318 debounce_cc(func, delay) { 319 let timer; 320 return function (...args) { 321 clearTimeout(timer); 322 timer = setTimeout(() => func.apply(this, args), delay); 323 }; 324 } 325 326 bindCheckoutEvents() { 327 $('form.checkout').on('click', 'input[name="payment_method"]', () => { 328 this.debouncedTogglePlaceOrderButton(); 329 $(document.body).trigger('wpg_change_method'); 330 }); 331 $('#order_review').on('click', 'input[name="payment_method"]', () => { 332 this.debouncedTogglePlaceOrderButton(); 333 $(document.body).trigger('wpg_change_method'); 334 }); 335 if (this.ppcp_manager.is_pay_page === 'yes') { 336 this.debouncedTogglePlaceOrderButton(); 337 } 338 $(document.body).on('ppcp_cc_checkout_updated ppcp_checkout_updated ppcp_cc_block_ready ppcp_block_ready', () => { 339 this.debouncedTogglePlaceOrderButton(); 340 }); 341 $('form.checkout').on('checkout_place_order_wpg_paypal_checkout_cc', (event) => { 342 event.preventDefault(); 343 return this.handleCheckoutSubmit(event); 344 }); 345 346 // Any checkout error ends the in-flight card submit, so release the latch and 347 // let the buyer retry rather than leaving the Place order button inert. 348 $(document.body).on('checkout_error ppcp_checkout_error', () => { 349 this.clearCardFieldsSubmitLatch(); 350 }); 351 352 $('#order_review').on('submit', (event) => { 353 if (this.isPpcpCCSelected()) { 354 event.preventDefault(); 355 return this.handleCheckoutSubmit(); 356 } 357 }); 358 359 const eventSelectors = 'added_to_cart updated_cart_totals wc_fragments_refreshed wc_fragment_refresh wc_fragments_loaded updated_checkout ppcp_block_ready ppcp_checkout_updated ppcp_minicart_ready ppcp_minicart_updated wc_update_cart wc_cart_emptied wpg_change_method fkwcs_express_button_init'; 360 const checkoutSelectors = 'updated_cart_totals wc_fragments_refreshed wc_fragments_loaded updated_checkout ppcp_cc_block_ready ppcp_cc_checkout_updated update_checkout wpg_change_method'; 361 $(document.body).on(eventSelectors, (event) => { 362 this.debouncedUpdatePaypalCheckout(); 363 if (this.ppcp_manager.enabled_google_pay === 'yes') { 364 this.debouncedUpdateGooglePay(); 365 } 366 if (this.ppcp_manager.enabled_apple_pay === 'yes') { 367 this.debouncedUpdateApplePay(); 368 } 369 }); 370 if (this.ppcp_manager.advanced_card_payments === 'yes') { 371 $(document.body).on(checkoutSelectors, () => { 372 this.debouncedUpdatePaypalCC(); 373 }); 374 } 375 376 } 377 378 handleCheckoutSubmit() { 379 var fastlaneToken = $('#wpg_ppcp_fastlane_token').val() || window.wpgPPCPFastlaneToken || ''; 380 if (fastlaneToken) { 381 // Fastlane already tokenized the card; the single-use token rides 382 // along with the normal WooCommerce submit and is charged 383 // server-side, so skip the CardFields round-trip entirely. 384 return true; 385 } 386 var selected = $('input[name="wc-wpg_paypal_checkout_cc-payment-token"]:checked').val(); 387 if (selected && selected !== 'new') { 388 return true; // only return true for saved token (not for 'new') 389 } 390 if (this.isPpcpCCSelected() && this.isCardFieldEligible()) { 391 // The latch prevents a double submit while the CardFields round-trip â 392 // which includes any 3-D Secure challenge â is in flight. It used to be 393 // cleared only by handleCardFieldsError, so a challenge that never 394 // settled left it set forever: when the buyer authorises in their banking 395 // app the OS can discard the challenge popup, cardFields.submit() never 396 // resolves or rejects, and from then on every Place order
396click silently 397 // did nothing. Expire the latch so the buyer can always try again. 398 const $form = $(this.getCheckoutSelectorCss()); 399 if ($form.hasClass('paypal_cc_submitting')) { 400 const startedAt = parseInt($form.attr('data-wpg-cc-submitting-at') || '0', 10); 401 if (startedAt && (Date.now() - startedAt) < this.ccSubmitLatchTimeout) { 402 return false; 403 } 404 this.clearCardFieldsSubmitLatch(); 405 } 406 $form.addClass('paypal_cc_submitting').attr('data-wpg-cc-submitting-at', String(Date.now())); 407 $(document.body).trigger('submit_paypal_cc_form'); 408 return false; 409 } 410 return true; 411 } 412 413 /** 414 * Release the card-submit latch so the Place order button works again. 415 */ 416 clearCardFieldsSubmitLatch() { 417 $('form.checkout, form.cart, #order_review') 418 .removeClass('paypal_cc_submitting') 419 .removeAttr('data-wpg-cc-submitting-at'); 420 } 421 422 syncPayPalCheckoutButtons() { 423 this.ppcp_cart_css(); 424 this.renderSmartButton(); 425 if ($('#ppcp_checkout_top').length === 0) { 426 const $applePay = $('div.apple-pay-container[data-context="express_checkout"]'); 427 const $googlePay = $('div.google-pay-container[data-context="express_checkout"]'); 428 const hasAppleOnly = $applePay.length > 0 && $googlePay.length === 0; 429 const hasGoogleOnly = $googlePay.length > 0 && $applePay.length === 0; 430 if (hasAppleOnly && !$applePay.hasClass('mobile')) { 431 $applePay.css('min-width', '480px'); 432 } 433 if (hasGoogleOnly && !$googlePay.hasClass('mobile')) { 434 $googlePay.css('min-width', '480px'); 435 } 436 } 437 438 } 439 440 syncPayPalCardFields() { 441 if (this.isCardFieldEligible()) { 442 this.renderCardFields(); 443 $('#place_order, .wc-block-components-checkout-place-order-button').show(); 444 } else { 445 $('.wc_payment_method.payment_method_wpg_paypal_checkout_cc').hide(); 446 $('#radio-control-wc-payment-method-options-wpg_paypal_checkout_cc').parent('label').parent('div').hide(); 447 if (this.isPpcpCCSelected()) 448 $('#payment_method_wpg_paypal_checkout').prop('checked', true).trigger('click'); 449 } 450 451 } 452 453 isPpcpSelected() { 454 if (this.ppcp_manager.is_wpg_change_payment_method === 'yes') { 455 return false; 456 } 457 return $('#payment_method_wpg_paypal_checkout').is(':checked') || $('input[name="radio-control-wc-payment-method-options"]:checked').val() === 'wpg_paypal_checkout'; 458 } 459 460 isPpcpCCSelected() { 461 return $('#payment_method_wpg_paypal_checkout_cc').is(':checked') || $('input[name="radio-control-wc-payment-method-options"]:checked').val() === 'wpg_paypal_checkout_cc'; 462 } 463 464 isCardFieldEligible() { 465 return this.isCheckoutPage() && this.ppcp_manager.advanced_card_payments === 'yes' && typeof wpg_paypal_sdk !== 'undefined' && wpg_paypal_sdk.CardFields().isEligible(); 466 } 467 468 togglePlaceOrderButton() { 469 const isPpcpSelected = this.isPpcpSelected(); 470 const isPpcpCCSelected = this.isPpcpCCSelected(); 471 const usePlaceOrder = this.ppcp_manager.use_place_order === '1'; 472 const PO_CLASSIC = '#place_order'; 473 const PO_BLOCKS = '.wc-block-components-checkout-place-order-button'; 474 const WALLETS = '#ppcp_checkout, #ppcp_order_pay, .google-pay-container.checkout, .apple-pay-container.checkout'; 475 const setVisible = (sel, visible, displayShown = '') => { 476 const $el = jQuery(sel); 477 $el.toggleClass('wpg_place_order_hide', !visible); 478 $el.css('display', visible ? (displayShown || '') : 'none'); 479 }; 480 const showPO = () => { 481 setVisible(PO_CLASSIC, true, 'inline-block'); 482 setVisible(PO_BLOCKS, true, 'inline-flex'); 483 }; 484 const hidePO = () => { 485 setVisible(PO_CLASSIC, false); 486 setVisible(PO_BLOCKS, false); 487 }; 488 const showWallets = () => setVisible(WALLETS, true); 489 const hideWallets = () => setVisible(WALLETS, false); 490 if (isPpcpSelected) { 491 if (usePlaceOrder) { 492 hideWallets(); 493 showPO(); 494 } else { 495 hidePO(); 496 showWallets(); 497 } 498 } else { 499 showPO(); 500 hideWallets(); 501 } 502 if (isPpcpCCSelected && this.isCardFieldEligible()) { 503 showPO(); 504 hideWallets(); 505 } 506 } 507 508 onShippingChange(data, actions) { 509 if (!data || !data.shipping_address) { 510 return actions.reject(); 511 } 512 513 // Simple address mapping (same as before) 514 const shippingAddress = { 515 city: data.shipping_address.city || '', 516 state: data.shipping_address.state || '', 517 countryCode: data.shipping_address.country_code || '', 518 postalCode: data.shipping_address.postal_code || '' 519 }; 520 521 // NEW: detect which shipping method was selected in the PayPal UI 522 const selectedOption = 523 data.selected_shipping_option || 524 data.selectedShippingOption || 525 null; 526 527 const selectedShippingId = 528 selectedOption && selectedOption.id ? selectedOption.id : ''; 529 530 // Validate address via AJAX (this will also update PayPal order) 531 return fetch(this.ppcp_manager.ajax_url, { 532 method: 'POST', 533 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 534 body: new URLSearchParams({ 535 action: 'ppcp_validate_shipping_address', 536 security: this.ppcp_manager.ajax_nonce, 537 shipping_address: JSON.stringify(shippingAddress), 538 // NEW: send selected shipping method id to PHP 539 selected_shipping_id: selectedShippingId 540 }) 541 }) 542 .then(res => res.json()) 543 .then(result => { 544 if (result && result.success) { 545 return actions.resolve(); 546 } else { 547 return actions.reject(); 548 } 549 }) 550 .catch(() => { 551 return actions.reject(); 552 }); 553 } 554 555 556 renderSmartButton() { 557 const selectors = this.ppcp_manager.button_selector; 558 $.each(selectors, (key, selector) => { 559 const elements = jQuery(".ppcp-button-container.ppcp_mini_cart"); 560 if (elements.length > 1) { 561 elements.slice(0, -1).remove(); 562 } 563 if (!$(selector).length || $(selector).children().length || typeof wpg_paypal_sdk === 'undefined') { 564 return; 565 } 566 const isExpressCheckout = selector === '#ppcp_checkout_top'; 567 const isMiniCart = selector === '#ppcp_mini_cart' || selector === '#ppcp_mini_cart_block'; 568 const ppcpStyle = { 569 layout: isMiniCart 570 ? this.ppcp_manager.mini_cart_style_layout 571 : (isExpressCheckout ? this.ppcp_manager.express_checkout_style_layout : this.ppcp_manager.style_layout), 572 color: isMiniCart 573 ? this.ppcp_manager.mini_cart_style_color 574 : (isExpressCheckout ? this.ppcp_manager.express_checkout_style_color : this.ppcp_manager.style_color), 575 shape: isMiniCart 576 ? this.ppcp_manager.mini_cart_style_shape 577 : (isExpressCheckout ? this.ppcp_manager.express_checkout_style_shape : this.ppcp_manager.style_shape), 578 label: isMiniCart 579 ? this.ppcp_manager.mini_cart_style_label 580 : (isExpressCheckout ? this.ppcp_manager.express_checkout_style_label : this.ppcp_manager.style_label), 581 height: Number( 582 isExpressCheckout 583 ? this.ppcp_manager.express_checkout_button_height 584 : isMiniCart 585 ? this.ppcp_manager.mini_cart_button_height 586 : this.ppcp_manager.button_height 587 ) || 48 588 }; 589 if (ppcpStyle.layout === 'horizontal') { 590 ppcpStyle.tagline = 'false'; 591 } 592 const baseH = parseFloat(ppcpStyle.height) || 48; // handles "48" or "48px" 593 const heightPx = `${Math.max(0, baseH - 1)}px`; // reduce by 1px (no negatives) 594 595 const targets = []; 596 if (isMiniCart || selector === '#ppcp_cart') {
597 targets.push('#ppcp_cart', '#ppcp_mini_cart', '#ppcp_mini_cart_block', '.google-pay-container.cart', '.apple-pay-container.cart', '.google-pay-container.mini_cart', '.apple-pay-container.mini_cart'); 598 } else if (isExpressCheckout) { 599 targets.push('#ppcp_checkout_top', '#ppcp_checkout_top_alternative', 600 '.google-pay-container.express_checkout', '.apple-pay-container.express_checkout'); 601 } else if (selector === '#ppcp_product') { 602 targets.push('#ppcp_product', '.google-pay-container.product', '.apple-pay-container.product'); 603 } else if (selector === '#ppcp_checkout') { 604 targets.push('#ppcp_checkout', '.google-pay-container.checkout', '.apple-pay-container.checkout'); 605 } 606 607 if (targets.length) { 608 document.querySelectorAll(targets.join(',')).forEach(el => { 609 el.style.setProperty('--button-height', heightPx); 610 }); 611 } 612 613 const styledFundingSources = [ 614 wpg_paypal_sdk.FUNDING.PAYPAL, 615 wpg_paypal_sdk.FUNDING.PAYLATER 616 ]; 617 618 const addShippingCallbacks = selector !== '#ppcp_checkout'; 619 620 if (selector === '#ppcp_checkout') { 621 let fundingSources = wpg_paypal_sdk.getFundingSources(); 622 if (fundingSources.length) { 623 const hideTagline = this.ppcp_manager.is_google_pay_enabled_checkout === 'yes' || this.ppcp_manager.is_apple_pay_enable_checkout === 'yes'; 624 fundingSources.forEach((fundingSource) => { 625 if (fundingSource === wpg_paypal_sdk.FUNDING.CARD && this.isCardFieldEligible()) { 626 return; 627 } 628 const options = { 629 fundingSource, 630 onClick: async (data, actions) => { 631 this.ppcp_used_payment_method = fundingSource; 632 633 if (this.ppcp_manager.is_block_enable === 'yes' && window.wp?.data) { 634 const vDisp = wp.data.dispatch('wc/store/validation'); 635 const vSel = wp.data.select('wc/store/validation'); 636 const cartSel = wp.data.select('wc/store/cart'); 637 const chkSel = wp.data.select('wc/store/checkout'); 638 639 // 0) Wait until Blocks is idle (addresses/rates/coupons/customer updates) 640 const waitUntilIdle = async () => { 641 for (let i = 0; i < 30; i++) { 642 const busy = 643 cartSel?.isLoading?.() || 644 cartSel?.isCouponsUpdating?.() || 645 cartSel?.isCustomerDataUpdating?.() || 646 chkSel?.isProcessing?.(); // some builds expose this 647 if (!busy) 648 return; 649 await new Promise(r => setTimeout(r, 50)); 650 } 651 }; 652 await waitUntilIdle(); 653 654 // 1) Ask Blocks to reveal native messages 655 vDisp.showAllValidationErrors(); 656 657 // 2) Give the validation store a tick to recalc & render 658 await new Promise(r => setTimeout(r, 0)); 659
660 // 3) Re-check using BOTH the boolean and the map (some builds lag the boolean) 661 const hasErrorsBool = 662 typeof vSel.hasValidationErrors === 'function' && vSel.hasValidationErrors(); 663 const errorMap = typeof vSel.getValidationErrorMap === 'function' 664 ? vSel.getValidationErrorMap() 665 : {}; 666 const hasErrors = hasErrorsBool || (errorMap && Object.keys(errorMap).length > 0); 667 668 if (hasErrors) { 669 // keep focus on form; errors are already shown natively 670 return actions?.reject ? actions.reject() : false; 671 } 672 } 673 674 // proceed to wallet 675 return true; 676 } 677 , 678 createOrder: () => this.createOrder(selector), 679 onApprove: (data, actions) => this.onApproveHandler(data, actions), 680 onCancel: () => this.onCancelHandler(), 681 onError: (err) => this.onErrorHandler(err) 682 }; 683 let style = {...ppcpStyle}; 684 if (hideTagline) { 685 style.tagline = 'false'; 686 const {layout, ...base} = style; 687 style = base; // remove layout 688 } 689 let cleanStyleBase = {...style}; 690 if (styledFundingSources.includes(fundingSource)) { 691 options.style = {...cleanStyleBase}; 692 } else { 693 const {color, ...cleanStyle} = cleanStyleBase; 694 options.style = {...cleanStyle}; 695 } 696 const button = wpg_paypal_sdk.Buttons(options); 697 if (button.isEligible()) { 698 button.render(selector); 699 } 700 }); 701 } 702 } else if (selector === '#ppcp_checkout_top') { 703 704 const expressFundingSources = [ 705 wpg_paypal_sdk.FUNDING.PAYPAL, 706 wpg_paypal_sdk.FUNDING.VENMO, 707 wpg_paypal_sdk.FUNDING.PAYLATER, 708 wpg_paypal_sdk.FUNDING.CREDIT 709 ]; 710 const renderSelectors = ['#ppcp_checkout_top', '#ppcp_checkout_top_alternative']; 711 let renderedCount = 0; 712 for (let i = 0; i < expressFundingSources.length; i++) { 713 const fundingSource = expressFundingSources[i]; 714 const targetSelector = renderSelectors[renderedCount]; 715 if (!targetSelector || !$(targetSelector).length) { 716 continue; 717 } 718 const {layout, ...cleanStyle} = ppcpStyle; 719 let style = {...cleanStyle}; 720 if (fundingSource === wpg_paypal_sdk.FUNDING.VENMO) { 721 style = { 722 ...style, 723 color: 'blue' 724 }; 725 } else if (fundingSource === wpg_paypal_sdk.FUNDING.CREDIT) { 726 style = { 727 ...style, 728 color: 'darkblue' 729 }; 730 } 731 const options = { 732 style, 733 fundingSource, 734 createOrder: () => this.createOrder(targetSelector), 735 onApprove: (data, actions) => this.onApproveHandler(data, actions), 736 onCancel: () => this.onCancelHandler(), 737 onError: (err) => this.onErrorHandler(err), 738 // Add this line only: 739 onShippingChange: addShippingCallbacks ? (data, actions) => this.onShippingChange(data, actions) : null 740 }; 741 const button = wpg_paypal_sdk.Buttons(options); 742 if (button.isEligible()) { 743 button.render(targetSelector); 744 renderedCount++; 745 } 746 if (renderedCount >= 2) { 747 break; 748 } 749 } 750 if (renderedCount < 2 && $('#ppcp_checkout_top_alternative').length) { 751 if ($('div.apple-pay-container[data-context="express_checkout"]').length === 0 && $('div.google-pay-container[data-context="express_checkout"]').length === 0) { 752 if ($('#ppcp_checkout_top').length && !$('#ppcp_checkout_top').hasClass('mobile')) { 753 $('#ppcp_checkout_top').css('min-width', '480px'); 754 } 755 } 756 $('#ppcp_checkout_top_alternative').remove(); 757 } 758 759 } else { 760 const buttonOptions = { 761 style: ppcpStyle, 762 createOrder: () => this.createOrder(selector), 763 onApprove: (data, actions) => this.onApproveHandler(data, actions), 764 onCancel: () => this.onCancelHandler(), 765 onError: (err) => this.onErrorHandler(err) 766 }; 767 if (addShippingCallbacks) { 768 buttonOptions.onShippingChange = (data, actions) => this.onShippingChange(data, actions); 769 } 770 wpg_paypal_sdk.Buttons(buttonOptions).render(selector); 771 } 772 }); 773 var $targets = $('#ppcp_product, #ppcp_cart, #ppcp_mini_cart, #ppcp_mini_cart_block, #ppcp_checkout, #ppcp_checkout_top, #ppcp_checkout_top_alternative'); 774 setTimeout(function () { 775 $targets.css({background: '', 'background-color': ''}); 776 $targets.each(function () { 777 this.style.setProperty('--wpg-skel-fallback-bg', 'transparent'); 778 }); 779 $targets.addClass('bg-cleared'); 780 }, 1); 781 } 782 783 getFromValue(selector) { 784 if (selector === '#ppcp_product') return 'product'; 785 if (selector === '#ppcp_cart') return 'cart'; 786 if (selector === '#ppcp_mini_cart' || selector === '#ppcp_mini_cart_block') return 'cart'; 787 if (selector === '#ppcp_checkout_top') return 'express_checkout'; 788 if (selector === '#ppcp_checkout') return 'checkout'; 789 if (selector === '#ppcp_order_pay') return 'pay_page'; 790 791 return 'checkout'; // fallback default 792 } 793 794 /** 795 * Re-issue the nonces in ppcp_manager for the session as it stands right now. 796 * 797 * Everything localized into ppcp_manager was minted for whoever the shopper was 798 * when the page loaded. WooCommerce can log them in without a reload - ticking 799 * "create an account" and having the first attempt declined on another gateway 800 * is the usual way in - and from that moment the page-load nonces are rejected, 801 * so create_order fails inside WC_Checkout::process_checkout() with the generic 802 * "We were unable to process your order, please try again." however valid the 803 * card is, until the shopper reloads. Fetching a matching pair immediately 804 * before each submit keeps this flow honest the way the Store API's 805 * per-response nonce does for the rest of the block checkout. 806 * 807 * Concurrent callers share the one request, and a refresh that fails leaves the 808 * existing values in place, so this can only ever improve the odds. 809 * 810 * @param {string}
810 [data] The serialized payload about to be submitted. Passing 811 * it lets a submit that carries no nonce skip the 812 * round-trip entirely. Omit to refresh unconditionally. 813 * @param {boolean} [append] Whether the caller will add a nonce to a payload that 814 * has none, as the block checkout does. 815 * @return {Promise<string>} the checkout nonce to submit with. 816 */ 817 refreshCheckoutNonce(data, append = false) { 818 const current = () => this.ppcp_manager.woocommerce_process_checkout; 819 // Product, cart and mini-cart buttons post to create_order branches that never 820 // reach process_checkout(), so they submit no nonce and must not be made to 821 // wait on one. Only a payload that carries a nonce, or is about to be given 822 // one, has anything to gain here. 823 if (arguments.length && !append && !CHECKOUT_NONCE_FIELD.test(data)) { 824 return Promise.resolve(current()); 825 } 826 if (!this.ppcp_manager.refresh_nonce_url) { 827 return Promise.resolve(current()); 828 } 829 if (!this.checkoutNonceRequest) { 830 this.checkoutNonceRequest = fetch(this.ppcp_manager.refresh_nonce_url, { 831 method: 'GET', 832 credentials: 'same-origin', 833 cache: 'no-store', 834 headers: {'X-Requested-With': 'XMLHttpRequest'} 835 }) 836 .then(res => res.json()) 837 .then(body => { 838 const nonces = (body && body.success) ? body.data : null; 839 if (nonces && nonces.woocommerce_process_checkout) { 840 this.ppcp_manager.woocommerce_process_checkout = nonces.woocommerce_process_checkout; 841 } 842 // The wallet shipping/total endpoints verify this one and answer a 843 // stale value with a 403, so it goes stale on the same login. 844 if (nonces && nonces.ajax_nonce) { 845 this.ppcp_manager.ajax_nonce = nonces.ajax_nonce; 846 } 847 return current(); 848 }) 849 .catch(() => current()) 850 .then((nonce) => { 851 this.checkoutNonceRequest = null; 852 return nonce; 853 }); 854 } 855 return this.checkoutNonceRequest; 856 } 857 858 /** 859 * Point an already-serialized checkout payload at `nonce`. 860 * 861 * The classic checkout form carries its own page-load 862 * woocommerce-process-checkout-nonce into serialize(), so replacing in place is 863 * what makes the refresh reach that flow too. `append` covers the block 864 * checkout, whose form has no such field for serialize() to pick up. 865 */ 866 applyCheckoutNonce(data, nonce, append = false) { 867 if (typeof data !== 'string' || !nonce) { 868 return data; 869 } 870 const pair = 'woocommerce-process-checkout-nonce=' + encodeURIComponent(nonce); 871 if (CHECKOUT_NONCE_FIELD.test(data)) { 872 return data.replace(CHECKOUT_NONCE_FIELD, (_match, lead) => lead + pair); 873 } 874 if (!append) { 875 return data; 876 } 877 return data ? data + '&' + pair : pair; 878 } 879 880 /** 881 * The block checkout's "Create an account" choice, as form fields to append. 882 * 883 * Blocks renders that checkbox itself as a React control with no name 884 * attribute â the same way as the save-card checkbox mirrored onto 885 * window.wpgPPCPShouldSaveCard â so the jQuery.serialize() of the checkout 886 * form can never carry it. The Store API, which normally acts on the choice, 887 * is bypassed by every payload this flow posts to the plugin's own 888 * create_order endpoint, where WC_Checkout::process_checkout() only creates 889 * an account when a createaccount field is posted. Left out, a shopper who 890 * ticked the box was silently checked out as a guest. Read the choice from 891 * the Blocks data store at submit time, when it is current. 892 * 893 * On stores that don't auto-generate passwords, Blocks renders a "Create a
894 * password" field the same nameless way, and process_checkout() refuses 895 * createaccount without account_password â so the password has to travel 896 * with the flag or the shopper would be blocked with a validation error 897 * their screen says they already satisfied. 898 * 899 * The classic checkout needs nothing here â it renders real named inputs 900 * that serialize() picks up â and a payload that already carries a 901 * createaccount field is left alone rather than second-guessed. 902 * 903 * @param {string} [data] The serialized payload the fields will be appended to. 904 * @return {string} '&'-prefixed fields to append, or '' when there is nothing to say. 905 */ 906 blockCreateAccountFields(data) { 907 try { 908 if (typeof wp === 'undefined' || !wp.data?.select) { 909 return ''; 910 } 911 const checkout = wp.data.select('wc/store/checkout'); 912 if (!checkout?.getShouldCreateAccount?.()) { 913 return ''; 914 } 915 if (/(^|&)createaccount=/.test(data || '')) { 916 return ''; 917 } 918 let fields = '&createaccount=1'; 919 const password = checkout.getCustomerPassword?.(); 920 if (password) { 921 fields += '&account_password=' + encodeURIComponent(password); 922 } 923 return fields; 924 } catch (e) { 925 return ''; 926 } 927 } 928 929 createOrder(selector) { 930 this.showSpinner(); 931 $('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove(); 932 let data; 933 // Only the block checkout needs the nonce appended; every other branch either 934 // serializes a form that already carries one or posts to a create_order 935 // branch that never reaches process_checkout(). 936 let appendNonce = false; 937 const isMiniCart = selector === '#ppcp_mini_cart' || selector === '#ppcp_mini_cart_block'; 938 if (isMiniCart) { 939 data = ''; 940 } else if (selector === '#ppcp_checkout_top') { 941 } else if (this.isCheckoutPage()) { 942 data = $(selector).closest('form').serialize(); 943 if (this.ppcp_manager.is_block_enable === 'yes') { 944 const notes = jQuery('.wc-block-components-textarea').val() || ''; 945 data += '&customer_note=' + encodeURIComponent(notes); 946 const billingAddress = this.getBillingAddress(); 947 const shippingAddress = this.getShippingAddress(); 948 data += '&billing_address=' + encodeURIComponent(JSON.stringify(billingAddress)); 949 data += '&shipping_address=' + encodeURIComponent(JSON.stringify(shippingAddress)); 950 data += this.blockCreateAccountFields(data); 951 appendNonce = true; 952 } 953 } else if (this.isProductPage()) { 954 $('<input>', {type: 'hidden', name: 'ppcp-add-to-cart', value: $("[name='add-to-cart']").val()}).appendTo('form.cart'); 955 data = $('form.cart').serialize(); 956 } else { 957 data = $('form.woocommerce-cart-form').serialize(); 958 } 959 960 const fundingMethod = this.ppcp_used_payment_method; 961 const from = this.getFromValue(selector); 962 963 let createOrderUrl = this.ppcp_manager.create_order_url_for_paypal 964 .replace(/([?&])from=[^&]*/i, '$1') // remove old `from` 965 .replace(/[?&]$/, ''); // cleanup 966 967 createOrderUrl += (createOrderUrl.includes('?') ? '&' : '?') 968 + 'from=' + from 969 + '&ppcp_used_payment_method=' + encodeURIComponent(fundingMethod); 970 971 // Serialize first, then swap in a nonce minted for the session as it is now: 972 // the one from page load is rejected outright once the shopper has been 973 // logged in mid-checkout. Reading the form before the round-trip keeps the 974 // snapshot of the fields identical to what it has always been. 975 return this.refreshCheckoutNonce(data, appendNonce).then((nonce) => { 976 return fetch(createOrderUrl, { 977 method: 'POST', 978 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 979 body: this.applyCheckoutNonce(data, nonce, appendNonce) 980 }); 981 }).then(res => res.json()).then(data => { 982 this.hideSpinner(); 983 if (data.success !== undefined) { 984 const messages = data.data.messages ?? data.data; 985 this.showError(messages); 986 return null; 987 } 988 return data.orderID; 989 }); 990 } 991 992 googleapplecreateOrder() { 993 this.showSpinner(); 994 $('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove(); 995 let data = ''; 996 let appendNonce = false; 997 switch (this.pageContext) { 998 case 'checkout': 999 const selector = `[data-context="${this.pageContext}"]`; 1000 data = $(selector).closest('form').serialize(); 1001 const isBlockCheckout = this.ppcp_manager.is_block_enable === 'yes'; 1002 if (isBlockCheckout && $('form.wc-block-checkout__form').length) { 1003 const notes = jQuery('.wc-block-components-textarea').val() || ''; 1004 data += '&customer_note=' + encodeURIComponent(notes); 1005 let billingAddress = this.getBillingAddress();
1006 let shippingAddress = this.getShippingAddress(); 1007 // User-entered Block Checkout fields take precedence. Only fall back to 1008 // the wallet address when the customer clicked an express wallet button 1009 // without filling in the form, otherwise the WC order is created blank. 1010 if ((!billingAddress || !billingAddress.billing_address_1) && this.walletBillingAddress) { 1011 const walletBilling = this.walletToWCFormat(this.walletBillingAddress, 'billing'); 1012 if (walletBilling.billing_address_1) { 1013 billingAddress = walletBilling; 1014 } 1015 } 1016 if ((!shippingAddress || !shippingAddress.shipping_address_1) && this.walletShippingAddress) { 1017 const walletShipping = this.walletToWCFormat(this.walletShippingAddress, 'shipping'); 1018 if (walletShipping.shipping_address_1) { 1019 shippingAddress = walletShipping; 1020 } 1021 } 1022 data += '&billing_address=' + encodeURIComponent(JSON.stringify(billingAddress)); 1023 data += '&shipping_address=' + encodeURIComponent(JSON.stringify(shippingAddress)); 1024 data += this.blockCreateAccountFields(data); 1025 appendNonce = true; 1026 } else if ($('form.checkout').length) { 1027 data = $('form.checkout').serialize(); 1028 } else if ($('form.woocommerce-cart-form').length) { 1029 data = $('form.woocommerce-cart-form').serialize(); 1030 } 1031 break; 1032 case 'express_checkout': 1033 case 'product': 1034 break; 1035 default: 1036 data = $('form.woocommerce-cart-form').serialize(); 1037 break; 1038 } 1039 1040 let createOrderUrl = this.ppcp_manager.create_order_url_for_google_pay; 1041 1042 createOrderUrl = createOrderUrl 1043 .replace(/([?&])from=[^&]*/i, '$1') 1044 .replace(/[?&]$/, ''); 1045 1046 createOrderUrl += (createOrderUrl.includes('?') ? '&' : '?') 1047 + 'from=' + encodeURIComponent(this.pageContext); 1048 1049 // See createOrder(): the page-load nonce is dead once the shopper has been 1050 // logged in mid-checkout, so refresh it before this reaches process_checkout(). 1051 return this.refreshCheckoutNonce(data, appendNonce).then((nonce) => { 1052 return fetch(createOrderUrl, { 1053 method: 'POST', 1054 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 1055 body: this.applyCheckoutNonce(data, nonce, appendNonce) 1056 }); 1057 }).then(res => res.json()).then(data => { 1058 this.hideSpinner(); 1059 if (data.success !== undefined && data.success === false) { 1060 const messages = typeof data.data === 'string' 1061 ? data.data 1062 : (data.data.messages ?? 'An unknown error occurred.'); 1063 1064 this.showError(messages); 1065 throw new Error(messages); 1066 return null; 1067 } 1068 return data.orderID; 1069 }); 1070 } 1071 1072 onApproveHandler(data, actions) { 1073 this.showSpinner(); 1074 const order_id = data.orderID || data.orderId || ''; 1075 const payer_id = data.payerID || data.payerId || ''; 1076 if (!order_id) { 1077 this.hideSpinner(); 1078 this.showError(this.ppcp_manager.unknown_error || 'We could not process your PayPal approval. Please try again.'); 1079 return; 1080 } 1081 if (this.isCheckoutPage() || this.ppcp_manager.skip_order_review === 'yes') { 1082 const url = `${this.ppcp_manager.cc_capture}&paypal_order_id=${encodeURIComponent(order_id)}&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`; 1083 $.post(url, (response) => { 1084 if (response?.data?.redirect) { 1085 window.location.href = response.data.redirect; 1086 return; 1087 }
1088 // Any non-redirect response (explicit failure, or an unexpected/empty 1089 // body): stop the spinner and surface an error rather than hanging. 1090 this.hideSpinner(); 1091 const messages = response?.data?.messages 1092 ?? (this.ppcp_manager.unknown_error || 'An error occurred while completing your payment. If your account was charged, please contact us before trying again.'); 1093 this.showError(messages); 1094 if (response?.success === false) { 1095 let redirectUrl = `${this.ppcp_manager.checkout_url}?paypal_order_id=${encodeURIComponent(order_id)}&from=${this.ppcp_manager.page}`; 1096 if (payer_id) { 1097 redirectUrl += `&paypal_payer_id=${encodeURIComponent(payer_id)}`; 1098 } 1099 window.location.href = redirectUrl; 1100 } 1101 }).fail(() => { 1102 // Network/HTTP failure. The capture may have already succeeded server 1103 // side, so do not encourage a blind retry that could double-charge. 1104 this.hideSpinner(); 1105 this.showError('We could not confirm your payment. If your account was charged, please contact us before trying again.'); 1106 }); 1107 return; 1108 } 1109 let redirectUrl = `${this.ppcp_manager.checkout_url}?paypal_order_id=${encodeURIComponent(order_id)}&from=${this.ppcp_manager.page}`; 1110 if (payer_id) { 1111 redirectUrl += `&paypal_payer_id=${encodeURIComponent(payer_id)}`; 1112 } 1113 window.location.href = redirectUrl; 1114 } 1115 1116 showSpinner(containerSelector = '.woocommerce') { 1117 if (jQuery('.wc-block-checkout__main').length || jQuery('.wp-block-woocommerce-cart').length) { 1118 jQuery('.wc-block-checkout__main, .wp-block-woocommerce-cart').block({message: null, overlayCSS: {background: '#fff', opacity: 0.6}}); 1119 } else if (jQuery('form.checkout').length) { 1120 jQuery('form.checkout').block({message: null, overlayCSS: {background: '#fff', opacity: 0.6}}); 1121 } else if (jQuery(containerSelector).length) { 1122 jQuery(containerSelector).block({message: null, overlayCSS: {background: '#fff', opacity: 0.6}}); 1123 } 1124 1125 if (jQuery('#wfacp-sec-wrapper').length) { 1126 jQuery('#wfacp-sec-wrapper').block({message: null, overlayCSS: {background: '#fff', opacity: 0.6}}); 1127 } 1128 } 1129 1130 hideSpinner(containerSelector = '.woocommerce') { 1131 if (jQuery('.wc-block-checkout__main').length || jQuery('.wp-block-woocommerce-cart').length) { 1132 jQuery('.wc-block-checkout__main, .wp-block-woocommerce-cart').unblock(); 1133 } else if (jQuery('form.checkout').length) { 1134 jQuery('form.checkout').unblock(); 1135 } else if (jQuery(containerSelector).length) { 1136 jQuery(containerSelector).unblock(); 1137 } 1138 1139 if (jQuery('#wfacp-sec-wrapper').length) { 1140 jQuery('#wfacp-sec-wrapper').unblock(); 1141 } 1142 } 1143 1144 onCancelHandler() { 1145 window.location.reload(true); 1146 this.hideSpinner(); 1147 } 1148 1149 onErrorHandler(err) { 1150 this.hideSpinner(); 1151 } 1152 1153 showError(error_message) { 1154 if (typeof error_message === 'undefined' || error_message === null) { 1155 return; 1156 } 1157 let $checkout_form; 1158 if ($('form.checkout').length) { 1159 $checkout_form = $('form.checkout'); 1160 } else if ($('.woocommerce-notices-wrapper').length) { 1161 $checkout_form = $('.woocommerce-notices-wrapper'); 1162 } else if ($('.woocommerce').length) { 1163 $checkout_form = $('.woocommerce'); 1164 } else if ($('.wc-block-components-notices').length) { 1165 $checkout_form = $('.wc-block-components-notices').first(); 1166 } else if ($('form#wfacp_checkout_form').length) { 1167 $checkout_form = $('form#wfacp_checkout_form').first(); 1168 } 1169 if ($checkout_form && $checkout_form.length) { 1170 $('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove(); 1171 if (!error_message || (typeof error_message !== 'string' && !Array.isArray(error_message))) { 1172 error_message = ['An unknown error occurred.']; 1173 } else if (typeof error_message === 'string') { 1174 error_message = [error_message]; 1175 } else if (error_message?.data?.messages && Array.isArray(error_message.data.messages)) { 1176 error_message = error_message.data.messages; 1177 }
1178 let errorHTML = '<div class="woocommerce-NoticeGroup woocommerce-NoticeGroup-checkout" role="alert" aria-live="assertive"><ul class="woocommerce-error">'; 1179 $.each(error_message, (index, value) => { 1180 errorHTML += `<li>${value}</li>`; 1181 }); 1182 errorHTML += '</ul></div>'; 1183 $checkout_form.prepend(errorHTML).removeClass('processing').unblock(); 1184 $checkout_form.find('.input-text, select, input:checkbox').trigger('validate').trigger('blur'); 1185 const scrollElement = $('.woocommerce-NoticeGroup-updateOrderReview, .woocommerce-NoticeGroup-checkout').filter(function () { 1186 const $el = $(this); 1187 if (!$el.length || !$el.is(':visible')) { 1188 return false; 1189 } 1190 const offset = $el.offset?.(); 1191 return offset && typeof offset.top !== 'undefined'; 1192 }).first(); 1193 if (scrollElement.length) { 1194 const offset = scrollElement.offset(); 1195 if (offset && typeof offset.top !== 'undefined') { 1196 $('html, body').animate({scrollTop: offset.top - 100}, 1000); 1197 } 1198 } 1199 //$(document.body).trigger('checkout_error', [error_message]); 1200 } else { 1201 const errorMessagesString = Array.isArray(error_message) 1202 ? error_message.join('<br>') 1203 : typeof error_message === 'string' 1204 ? error_message 1205 : 'An unknown error occurred.'; 1206 1207 $(document.body).trigger('ppcp_checkout_error', errorMessagesString); 1208 } 1209 } 1210 1211 renderCardFields() { 1212 jQuery(document.body).trigger('wc-credit-card-form-init'); 1213 const checkoutSelector = this.getCheckoutSelectorCss(); 1214 if ($('#wpg_paypal_checkout_cc-card-number').length === 0 || typeof wpg_paypal_sdk === 'undefined') { 1215 return; 1216 } 1217 $(checkoutSelector).addClass('CardFields'); 1218 // renderCardFields() re-runs on every checkout refresh, but the hosted fields 1219 // are only mounted once. Only the CardFields instance that actually mounted 1220 // its fields can submit them, so when they survived the refresh the live 1221 // instance must be left alone â building another one here and pointing Place 1222 // order at it hands the click to an object the SDK refuses to submit, and 1223 // nothing happens. A refresh that wipes the fields empties the container, and 1224 // then we do want a fresh instance. 1225 const fieldsMounted = ($('#wpg_paypal_checkout_cc-card-number').html() || '').trim() !== ''; 1226 if (fieldsMounted && this.cardFieldsInstance) { 1227 return; 1228 } 1229 const cardStyle = { 1230 input: {fontSize: '18px', fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: '400', color: '#32325d', padding: '12px 14px', borderRadius: '4px', border: '1px solid #ccd0d5', background: '#ffffff', boxShadow: 'none', transition: 'border-color 0.15s ease, box-shadow 0.15s ease'}, 1231 '.invalid': {color: '#fa755a', border: '1px solid #fa755a', boxShadow: 'none'}, 1232 '::placeholder': {color: '#aab7c4'}, 1233 'input:focus': {outline: 'none', border: '1px solid #4a90e2', boxShadow: '0 0 4px rgba(74, 144, 226, 0.3)'}, 1234 '.valid': {border: '1px solid #3ac569', color: '#32325d', boxShadow: 'none'} 1235 }; 1236 const cardFields = wpg_paypal_sdk.CardFields({ 1237 style: cardStyle, 1238 createOrder: () => this.createCardOrder(checkoutSelector), 1239 onApprove: (payload) => { 1240 if (!payload || !payload.orderID) { 1241 console.error("No valid payload returned during onApprove:", payload); 1242 return; 1243 } 1244 // Returned so cardFields.submit() only settles once the capture has 1245 // finished â the block checkout waits on that. The no-op catch marks 1246 // the rejection handled so an SDK build that ignores this return value 1247 // cannot leave an "uncaught (in promise)" error in the console; the 1248 // rejection is still delivered to any SDK build that does await it. 1249 const capture = this.submitCardFields(payload); 1250 capture.catch(() => {}); 1251 return capture; 1252 }, 1253 onError: (err) => { 1254 this.handleCardFieldsError(err, checkoutSelector); 1255 } 1256 }); 1257 if (cardFields.isEligible()) { 1258 const numberField = cardFields.NumberField(); 1259 $("#wpg_paypal_checkout_cc-card-number").empty(); 1260 $("#wpg_paypal_checkout_cc-card-expiry").empty(); 1261 $("#wpg_paypal_checkout_cc-card-cvc").empty(); 1262 numberField.render("#wpg_paypal_checkout_cc-card-number"); 1263 numberField.setAttribute("placeholder", "1234 1234 1234 1234"); 1264 cardFields.ExpiryField().render("#wpg_paypal_checkout_cc-card-expiry"); 1265 cardFields.CVVField().render("#wpg_paypal_checkout_cc-card-cvc"); 1266 // Remember which instance owns the mounted fields; the guard above uses
1267 // it to leave a working card form untouched on the next refresh. 1268 this.cardFieldsInstance = cardFields; 1269 setTimeout(function () { 1270 $('.wpg-paypal-cc-field label, .wpg-ppcp-card-cvv-icon').show(); 1271 }, 1600); 1272 setTimeout(function () { 1273 $('.wpg_ppcp_sanbdox_notice').show(); 1274 }, 1900); 1275 } else { 1276 console.log('Advanced Card Payments not Eligible', cardFields.isEligible()); 1277 this.cardFieldsInstance = null; 1278 $('.payment_box.payment_method_wpg_paypal_checkout_cc').hide(); 1279 if (this.isPpcpCCSelected()) { 1280 $('#payment_method_wpg_paypal_checkout').prop('checked', true).trigger('click'); 1281 } 1282 } 1283 // renderCardFields() re-runs on every updated_checkout / fragment refresh, so 1284 // binding without unbinding accumulated one handler per re-render: a single 1285 // Place order click then fired cardFields.submit() once per handler, across 1286 // CardFields instances that were never rendered (the render above is guarded) 1287 // and whose createOrder callbacks raced to mint competing PayPal orders into 1288 // the same session slot. Only the newest instance may handle the submit. 1289 $(document.body).off('submit_paypal_cc_form.wpgppcp').on('submit_paypal_cc_form.wpgppcp', () => { 1290 cardFields.submit().catch((error) => { 1291 this.handleCardFieldsError(error, checkoutSelector); 1292 }); 1293 }); 1294 // Awaitable entry point for the block checkout. It resolves the live 1295 // instance at call time rather than closing over this render's, so it stays 1296 // correct across the re-renders that rebuild the fields. 1297 window.wpgPPCPSubmitCardFields = () => this.submitCardFieldsFlow(); 1298 } 1299 1300 createCardOrder(checkoutSelector) { 1301 this.showSpinner(); 1302 $('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove(); 1303 let data; 1304 if (this.ppcp_manager.is_block_enable === 'yes') { 1305 data = $('form.wc-block-checkout__form').serialize(); 1306 // WooCommerce Blocks disables every field while the checkout is 1307 // processing, and jQuery.serialize() skips disabled inputs â so by the 1308 // time this runs the serialized form is empty. The server keys its whole 1309 // block address mapping on radio-control-wc-payment-method-options, so 1310 // without it the billing details below are never applied and create-order 1311 // fails with "Billing ⦠is a required field" / "Invalid payment method". 1312 // Read the selection directly (:checked still works on a disabled radio) 1313 // and append it when serialize() did not supply it. 1314 if (!/(^|&)radio-control-wc-payment-method-options=/.test(data)) { 1315 let selectedMethod = $('input[name="radio-control-wc-payment-method-options"]:checked').val(); 1316 if (!selectedMethod && typeof wp !== 'undefined' && wp.data?.select) { 1317 try { 1318 selectedMethod = wp.data.select('wc/store/payment')?.getActivePaymentMethod?.(); 1319 } catch (e) {} 1320 } 1321 if (selectedMethod) { 1322 data += '&radio-control-wc-payment-method-options=' + encodeURIComponent(selectedMethod); 1323 } 1324 } 1325 const notes = jQuery('.wc-block-components-textarea').val() || ''; 1326 data += '&customer_note=' + encodeURIComponent(notes); 1327 const billingAddress = this.getBillingAddress(); 1328 const shippingAddress = this.getShippingAddress(); 1329 data += '&billing_address=' + encodeURIComponent(JSON.stringify(billingAddress)); 1330 data += '&shipping_address=' + encodeURIComponent(JSON.stringify(shippingAddress)); 1331 // Vaulting has to be requested when the PayPal order is created, so the 1332 // shopper's "Save payment information to my account" choice must ride along 1333 // with this request. On the block checkout that checkbox is a React control 1334 // with no name attribute, so serialize() above cannot see it; the block 1335 // integration mirrors the choice onto window.wpgPPCPShouldSaveCard for us.
1336 // Without this the card was simply never saved. The classic checkout needs 1337 // nothing here â it renders a real named input that serialize() picks up. 1338 if (window.wpgPPCPShouldSaveCard) { 1339 data += '&wc-wpg_paypal_checkout_cc-new-payment-method=true'; 1340 } 1341 // "Create an account" is the same kind of unnamed React control, so it 1342 // needs carrying too â without it the card payment succeeded but the 1343 // shopper who asked for an account was checked out as a guest. 1344 data += this.blockCreateAccountFields(data); 1345 } else { 1346 data = $(checkoutSelector).closest('form').serialize(); 1347 } 1348 // This request runs process_checkout() server side, so it needs a nonce that 1349 // matches the session as it is right now. The one localized at page load was 1350 // minted for whoever the shopper was then, and WooCommerce logging them in 1351 // part-way through checkout - creating their account during an attempt that 1352 // another gateway declined, say - kills it for the rest of the page's life. 1353 // Left stale, every card submit after that point came back as the generic 1354 // "We were unable to process your order, please try again." with the card 1355 // never reaching PayPal at all. 1356 const isBlockCheckout = this.ppcp_manager.is_block_enable === 'yes'; 1357 return this.refreshCheckoutNonce(data, isBlockCheckout) 1358 .then(nonce => fetch(this.ppcp_manager.create_order_url_for_cc, { 1359 method: 'POST', 1360 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 1361 body: this.applyCheckoutNonce(data, nonce, isBlockCheckout) 1362 })) 1363 .then(res => res.json()) 1364 .then(data => { 1365 if (!data || data.success === false) { 1366 const messages = data?.data?.messages ?? data?.data; 1367 this.hideSpinner(); 1368 this.showError(messages || 'An unknown error occurred while creating the order.'); 1369 return Promise.reject(); 1370 } 1371 return data.orderID; 1372 }) 1373 .catch(err => { 1374 this.hideSpinner(); 1375 return Promise.reject(); 1376 }); 1377 } 1378 1379 /** 1380 * Capture the approved PayPal order and send the buyer to the thank-you page. 1381 * 1382 * Returns a promise so callers can wait for the payment to actually finish. 1383 * The block checkout needs that: it has to keep WooCommerce Blocks out of the 1384 * way until the CardFields round-trip (3-D Secure included) has completed. 1385 *
1386 * Rejections are already reported to the shopper here, so they carry the 1387 * "Expected reject" marker that handleCardFieldsError() treats as 1388 * already-handled â that keeps the classic checkout, whose submit handler 1389 * pipes rejections into handleCardFieldsError(), from showing the same error 1390 * a second time. 1391 */ 1392 submitCardFields(payload) { 1393 return new Promise((resolve, reject) => { 1394 const alreadyReported = (message) => reject(new Error('Expected reject: ' + message)); 1395 $.post(`${this.ppcp_manager.cc_capture}&paypal_order_id=${payload.orderID}&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`, (data) => { 1396 if (data?.data?.redirect) { 1397 // A refused capture â PayPal declining the card for fraud or AVS, 1398 // say â is still answered with a redirect, back to the checkout 1399 // rather than to the thank-you page. Navigate either way (the 1400 // decline notice is rendered on the page we land on), but only 1401 // report success when the payment actually succeeded: the block 1402 // checkout turns a resolved promise into "payment method ready" 1403 // and would place the order for a card that was declined. 1404 const declined = data?.data?.result === 'failure'; 1405 window.location.href = data.data.redirect; 1406 if (declined) { 1407 alreadyReported('the payment was refused'); 1408 return; 1409 } 1410 resolve(data.data.redirect); 1411 return; 1412 } 1413 // No redirect (server error or unexpected/empty body): show the error 1414 // instead of navigating to '/undefined'. Release the submit latch too, 1415 // otherwise the Place order button stays dead after the error. 1416 this.hideSpinner(); 1417 this.clearCardFieldsSubmitLatch(); 1418 const messages = data?.data?.messages 1419 ?? (this.ppcp_manager.unknown_error || 'An error occurred while completing your payment. If your account was charged, please contact us before trying again.'); 1420 this.showError(messages); 1421 alreadyReported(Array.isArray(messages) ? messages.join(' ') : String(messages)); 1422 }).fail(() => { 1423 this.hideSpinner(); 1424 this.clearCardFieldsSubmitLatch(); 1425 const message = 'We could not confirm your payment. If your account was charged, please contact us before trying again.'; 1426 this.showError(message); 1427 alreadyReported(message); 1428 }); 1429 }); 1430 } 1431 1432 /** 1433 * Run the whole card round-trip and resolve only once it has settled. 1434 * 1435 * Exposed on window as wpgPPCPSubmitCardFields for the block checkout. The 1436 * classic checkout keeps using the submit_paypal_cc_form event and is not 1437 * affected by this method. 1438 */ 1439 submitCardFieldsFlow() { 1440 const checkoutSelector = this.getCheckoutSelectorCss(); 1441 if (!this.cardFieldsInstance) { 1442 return Promise.reject(new Error('PayPal card fields are not ready yet. Please try again.')); 1443 } 1444 return this.cardFieldsInstance.submit().catch((error) => { 1445 // handleCardFieldsError() puts the message on the page itself, so flag the 1446 // rejection as already-reported and let the caller stay quiet about it. 1447 this.handleCardFieldsError(error, checkoutSelector); 1448 // Read .message rather than testing instanceof Error â the SDK rejects 1449 // from inside its own iframe, so those are not instances of our Error. 1450 const raw = (error && error.message) ? String(error.message) : String(error); 1451 throw raw.indexOf('Expected reject') !== -1 ? error : new Error('Expected reject: ' + raw); 1452 }); 1453 } 1454 1455 handleCardFieldsError(errorString, checkoutSelector) { 1456 $('#place_order, #wc-wpg_paypal_checkout-cc-form').unblock(); 1457 $(checkoutSelector).removeClass('processing paypal_cc_submitting CardFields createOrder').unblock(); 1458 this.clearCardFieldsSubmitLatch(); 1459 1460 const t = (key, fallback) => { 1461 const v = (this.ppcp_manager && this.ppcp_manager[key]) ? String(this.ppcp_manager[key]) : ''; 1462 return v ? v : fallback; 1463 }; 1464 1465 const UNKNOWN_FALLBACK = "An unknown error occurred with your payment. Please try again.";
1466 const INVALID_NUM_FALLBACK = "Please enter a valid card number."; 1467 const INVALID_CVV_FALLBACK = "Please enter a valid CVV."; 1468 const INVALID_EXP_FALLBACK = "Please enter a valid expiration date."; 1469 1470 let message = t('unknown_error', UNKNOWN_FALLBACK); 1471 1472 let raw = errorString instanceof Error ? errorString.message : String(errorString); 1473 if (raw.includes('Expected reject')) { 1474 return true; 1475 } 1476 1477 try { 1478 if (raw.includes('INVALID_NUMBER')) { 1479 message = t('invalid_number', INVALID_NUM_FALLBACK); 1480 } else if (raw.includes('INVALID_CVV')) { 1481 message = t('invalid_cvv', INVALID_CVV_FALLBACK); 1482 } else if (raw.includes('INVALID_EXPIRATION') || raw.includes('INVALID_EXPIRY_DATE')) { 1483 message = t('invalid_expiry', INVALID_EXP_FALLBACK); 1484 } else { 1485 const jsonStart = raw.indexOf('{'); 1486 const jsonEnd = raw.lastIndexOf('}') + 1; 1487 1488 if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) { 1489 const jsonString = raw.slice(jsonStart, jsonEnd); 1490 const error = JSON.parse(jsonString); 1491 1492 if ( 1493 (error.message && error.message.includes('Expected reject')) || 1494 (error.details?.[0]?.description && error.details[0].description.includes('Expected reject')) 1495 ) { 1496 return true; 1497 } 1498 1499 if (error.success === false && error.data && error.data.messages && Array.isArray(error.data.messages)) { 1500 if (error.data.messages.some(msg => msg.includes('Expected reject'))) { 1501 return true; 1502 } 1503 message = error.data.messages 1504 .map(msg => msg.replace(/<\/?[^>]+(>|$)/g, '')) 1505 .join('\n'); 1506 1507 } else if (error.details && Array.isArray(error.details)) { 1508 const cardError = error.details.find(detail => 1509 detail.issue === 'VALIDATION_ERROR' || 1510 detail.issue === 'INVALID_NUMBER' || 1511 detail.issue === 'INVALID_CVV' || 1512 detail.issue === 'INVALID_EXPIRY_DATE' || 1513 detail.field?.includes('/card/') 1514 ); 1515 1516 if (cardError) { 1517 switch (cardError.issue) { 1518 case 'INVALID_NUMBER': 1519 message = t('invalid_number', INVALID_NUM_FALLBACK); 1520 break; 1521 case 'INVALID_CVV': 1522 message = t('invalid_cvv', INVALID_CVV_FALLBACK); 1523 break; 1524 case 'INVALID_EXPIRY_DATE': 1525 message = t('invalid_expiry', INVALID_EXP_FALLBACK); 1526 break; 1527 case 'VALIDATION_ERROR': 1528 if (cardError.field?.includes('/card/number')) { 1529 message = t('invalid_number', INVALID_NUM_FALLBACK); 1530 } else if (cardError.field?.includes('/card/expiry')) { 1531 message = t('invalid_expiry', INVALID_EXP_FALLBACK); 1532 } else if (cardError.field?.includes('/card/security_code')) { 1533 message = t('invalid_cvv', INVALID_CVV_FALLBACK); 1534 } 1535 break; 1536 } 1537 } 1538 } 1539 1540 // If still default unknown message, try PayPal-provided message 1541 if (message === t('unknown_error', UNKNOWN_FALLBACK)) { 1542 if (error.details?.[0]?.description) { 1543 message = error.details[0].description; 1544 } else if (error.message) { 1545 message = error.message; 1546 } 1547 } 1548 } 1549 } 1550 } catch (err) {
1551 if (raw.includes('INVALID_NUMBER')) { 1552 message = t('invalid_number', INVALID_NUM_FALLBACK); 1553 } else if (raw.includes('INVALID_CVV')) { 1554 message = t('invalid_cvv', INVALID_CVV_FALLBACK); 1555 } else if (raw.includes('INVALID_EXPIRATION') || raw.includes('INVALID_EXPIRY_DATE')) { 1556 message = t('invalid_expiry', INVALID_EXP_FALLBACK); 1557 } else { 1558 const lastColon = raw.lastIndexOf(':'); 1559 message = lastColon > 0 ? raw.slice(lastColon + 1).trim() : raw; 1560 } 1561 } 1562 1563 message = String(message || '').replace(/\.$/, '').trim(); 1564 this.showError(message); 1565 this.hideSpinner(); 1566 return false; 1567 } 1568 1569 getCheckoutSelectorCss() { 1570 return this.isCheckoutPage() ? 'form.checkout' : 'form.cart'; 1571 } 1572 1573 isCCPaymentMethodSelected() { 1574 return this.getSelectedPaymentMethod() === 'wpg_paypal_checkout_cc'; 1575 } 1576 1577 getSelectedPaymentMethod() { 1578 return $('input[name="payment_method"]:checked').val(); 1579 } 1580 1581 ppcp_cart_css() { 1582 $('.payment_box.payment_method_wpg_paypal_checkout').each(function () { 1583 if (($(this).html() || '').trim() === '') { 1584 $(this).hide(); 1585 } 1586 }); 1587 } 1588 1589 manageVariations(selector) { 1590 if ($('.variations_form').length) { 1591 const self = this; 1592 const getCurrentProductId = () => { 1593 const variationInput = document.querySelector('input.variation_id'); 1594 const variationId = parseInt(variationInput?.value || 0, 10); 1595 return variationId > 0 ? variationId : parseInt(self.ppcp_manager?.product_id || 0, 10); 1596 }; 1597 const getQty = () => parseFloat(document.querySelector('input.qty')?.value || '1') || 1; 1598 1599 $('.variations_form, .single_variation').on('show_variation', function (event, variation) { 1600 if (variation.is_purchasable && variation.is_in_stock) { 1601 $(selector).show(); 1602 self.fetchProductTotal(variation.variation_id || getCurrentProductId(), getQty()); 1603 } else { 1604 $(selector).hide(); 1605 } 1606 }).on('hide_variation', function () { 1607 $(selector).hide(); 1608 }); 1609 1610 $(document).off('change.wpg_product_qty input.wpg_product_qty', 'input.qty') 1611 .on('change.wpg_product_qty input.wpg_product_qty', 'input.qty', () => { 1612 self.fetchProductTotal(getCurrentProductId(), getQty()); 1613 }); 1614 1615 // Product-option compat layers (Add-ons / TM EPO / WAPF) fire this 1616 // after updating their hidden cost fields; re-fetch so the wallet 1617 // total reflects the newly selected options. 1618 $(document.body).off('ppcp_checkout_updated.wpg_product_total') 1619 .on('ppcp_checkout_updated.wpg_product_total', () => { 1620 self.fetchProductTotal(getCurrentProductId(), getQty()); 1621 }); 1622 } 1623 } 1624 1625 manageSubscriptionOptions() { 1626 const $radios = $('input[name^="convert_to_sub"]'); 1627 if (!$radios.length) { 1628 return; 1629 } 1630 const toggleButtons = () => { 1631 const val = $('input[name^="convert_to_sub"]:checked').val(); 1632 const isSubscription = val && val !== '0'; 1633 $('.google-pay-container.product, .apple-pay-container.product').toggle(!isSubscription); 1634 $('#ppcp_product').find('[data-funding-source]').each(function () { 1635 const source = $(this).attr('data-funding-source'); 1636 if (source === 'paypal' || source === 'card') { 1637 $(this).show(); 1638 } else { 1639 $(this).toggle(!isSubscription); 1640 } 1641 }); 1642 }; 1643 $radios.on('change', toggleButtons); 1644 const observer = new MutationObserver(() => toggleButtons());
1645 const productContainer = document.getElementById('ppcp_product'); 1646 if (productContainer) { 1647 observer.observe(productContainer, { childList: true, subtree: true }); 1648 } 1649 toggleButtons(); 1650 } 1651 1652 isPayPalGooglePaySdkReady() { 1653 return typeof wpg_paypal_sdk !== "undefined" && typeof wpg_paypal_sdk.Googlepay !== "undefined" && typeof google !== "undefined"; 1654 } 1655 1656 removeGooglePayContainer() { 1657 const containers = document.querySelectorAll('.google-pay-container'); 1658 containers.forEach(container => { 1659 container.remove(); 1660 }); 1661 } 1662 1663 getGooglePaymentsClient( { requireOnPaymentDataChanged = false } = {}) { 1664 if (typeof google === "undefined") { 1665 return null; 1666 } 1667 if (!this.paymentsClient || this.paymentsClientRequiresDataChanged !== requireOnPaymentDataChanged) { 1668 const paymentDataCallbacks = { 1669 onPaymentAuthorized: this.onPaymentAuthorized.bind(this) 1670 }; 1671 if (requireOnPaymentDataChanged) { 1672 paymentDataCallbacks.onPaymentDataChanged = this.onPaymentDataChanged.bind(this); 1673 } 1674 this.paymentsClient = new google.payments.api.PaymentsClient({ 1675 environment: this.ppcp_manager.environment || "TEST", 1676 paymentDataCallbacks 1677 }); 1678 this.paymentsClientRequiresDataChanged = requireOnPaymentDataChanged; 1679 } 1680 return this.paymentsClient; 1681 } 1682 1683 async onPaymentDataChanged(intermediatePaymentData) { 1684 try { 1685 const {callbackTrigger, shippingAddress, shippingOptionData} = intermediatePaymentData || {}; 1686 const mapAddr = (addr = {}) => ({ 1687 address1: addr.address1 || '', 1688 address2: addr.address2 || '', 1689 city: addr.locality || '', 1690 state: addr.administrativeArea || '', 1691 postcode: addr.postalCode || '', 1692 country: addr.countryCode || '' 1693 }); 1694 if (callbackTrigger === 'INITIALIZE') { 1695 if (shippingAddress) { 1696 const mapped = mapAddr(shippingAddress); 1697 const vr = await this.validateShippingAddressFromBackend({ 1698 city: mapped.city, 1699 state: mapped.state, 1700 countryCode: mapped.country, 1701 postalCode: mapped.postcode 1702 }); 1703 if (!vr || vr.success !== true) { 1704 const msg = (vr && vr.data) ? String(vr.data) : this.t('shipping_unserviceable', 'We do not ship to this address.'); 1705 return { 1706 error: { 1707 reason: 'SHIPPING_ADDRESS_UNSERVICEABLE', 1708 message: msg, 1709 intent: 'SHIPPING_ADDRESS' 1710 } 1711 }; 1712 } 1713 await this.fetchUpdatedTotalFromBackend(mapped); 1714 const tx = this.getGoogleTransactionInfo(); 1715 tx.totalPriceStatus = 'ESTIMATED'; 1716 return { 1717 newTransactionInfo: tx, 1718 newShippingOptionParameters: this.getGoogleShippingOptionParametersOrPlaceholder() 1719 }; 1720 } 1721 return {}; 1722 } 1723 if (callbackTrigger === 'SHIPPING_ADDRESS' && shippingAddress) { 1724 const selectedShippingId = shippingOptionData && shippingOptionData.id ? shippingOptionData.id : ''; 1725 const mapped = mapAddr(shippingAddress); 1726 const vr = await this.validateShippingAddressFromBackend({ 1727 city: mapped.city, 1728 state: mapped.state, 1729 countryCode: mapped.country, 1730 postalCode: mapped.postcode 1731 }, selectedShippingId); 1732 if (!vr || vr.success !== true) { 1733 const msg = (vr && vr.data) ? String(vr.data) : this.t('shipping_unserviceable', 'We do not ship to this address.'); 1734 return { 1735 error: { 1736 reason: 'SHIPPING_ADDRESS_UNSERVICEABLE', 1737 message: msg, 1738 intent: 'SHIPPING_ADDRESS' 1739 } 1740 }; 1741 } 1742 await this.fetchUpdatedTotalFromBackend(mapped, null, selectedShippingId); 1743 const tx = this.getGoogleTransactionInfo(); 1744 tx.totalPriceStatus = 'ESTIMATED'; 1745 return { 1746 newTransactionInfo: tx, 1747 newShippingOptionParameters: this.getGoogleShippingOptionParametersOrPlaceholder() 1748 }; 1749 } 1750 if (callbackTrigger === 'SHIPPING_OPTION' && shippingOptionData && shippingOptionData.id) { 1751 if (shippingOptionData.id === 'pending') { 1752 const tx = this.getGoogleTransactionInfo(); 1753 tx.totalPriceStatus = 'ESTIMATED'; 1754 return { 1755 newTransactionInfo: tx, 1756 newShippingOptionParameters: this.getGoogleShippingOptionParametersOrPlaceholder() 1757 }; 1758 } 1759 const safeId = shippingOptionData.id; 1760 const internalId = (this.googleShippingIdMap && this.googleShippingIdMap[safeId]) ? this.googleShippingIdMap[safeId] : s
1760afeId; 1761 this.googleSelectedShippingId = internalId; 1762 const addr = shippingAddress ? mapAddr(shippingAddress) : {}; 1763 await this.fetchUpdatedTotalFromBackend(addr, null, internalId); 1764 const tx = this.getGoogleTransactionInfo(); 1765 tx.totalPriceStatus = 'ESTIMATED'; 1766 return { 1767 newTransactionInfo: tx, 1768 newShippingOptionParameters: this.getGoogleShippingOptionParametersOrPlaceholder() 1769 }; 1770 } 1771 return {}; 1772 } catch (e) { 1773 console.error('onPaymentDataChanged error:', e); 1774 return {}; 1775 } 1776 } 1777 1778 async getGooglePayConfig() { 1779 try { 1780 if (!this.allowedPaymentMethods || !this.merchantInfo) { 1781 const googlePayConfig = await wpg_paypal_sdk.Googlepay().config(); 1782 // PayPal reports merchant-level Google Pay eligibility here. When the 1783 // account is not provisioned for Google Pay, Google still issues a 1784 // token but PayPal cannot decrypt it, so confirmOrder fails at the 1785 // very end with INVALID_GOOGLE_PAY_TOKEN â don't offer the button. 1786 if (googlePayConfig && googlePayConfig.isEligible === false) { 1787 console.warn('[Google Pay] The connected PayPal merchant account is not eligible for Google Pay in this environment. Enable Google Pay on the PayPal account (Account Settings â Payment methods) or contact PayPal support.'); 1788 return { allowedPaymentMethods: [], merchantInfo: {} }; 1789 } 1790 let methods = googlePayConfig.allowedPaymentMethods || []; 1791 methods = methods.map(method => { 1792 const m = { ...method }; 1793 m.parameters = { ...(m.parameters || {}) }; 1794 m.parameters.billingAddressRequired = true; 1795 m.parameters.billingAddressParameters = { 1796 ...(m.parameters.billingAddressParameters || {}), 1797 // FULL returns the street-level billing address (address1/locality/ 1798 // etc.). The default MIN format only returns name, country and 1799 // postcode, which leaves the order billing address incomplete when 1800 // the checkout form is not filled in. 1801 format: 'FULL', 1802 phoneNumberRequired: true, 1803 }; 1804 return m; 1805 }); 1806 this.allowedPaymentMethods = methods; 1807 this.merchantInfo = googlePayConfig.merchantInfo || {}; 1808 this.googlePayApiVersion = googlePayConfig.apiVersion || 2; 1809 this.googlePayApiVersionMinor = googlePayConfig.apiVersionMinor || 0; 1810 // The Google Pay config response carries the PayPal account country; 1811 // feed it into transactionInfo.countryCode. The store base country 1812 // stays as fallback for SDK builds that omit it. 1813 if (googlePayConfig.countryCode) { 1814 this.googlePayMerchantCountry = googlePayConfig.countryCode; 1815 } 1816 } 1817 return { 1818 allowedPaymentMethods: this.allowedPaymentMethods, 1819 merchantInfo: this.merchantInfo, 1820 }; 1821 } catch (error) { 1822 console.error("Failed to fetch Google Pay configuration:", error); 1823 return { allowedPaymentMethods: [], merchantInfo: {} }; 1824 } 1825 } 1826 1827 getGoogleIsReadyToPayRequest(allowedPaymentMethods) { 1828 return { 1829 apiVersion: this.googlePayApiVersion || 2, 1830 apiVersionMinor: this.googlePayApiVersionMinor || 0, 1831 allowedPaymentMethods, 1832 }; 1833 } 1834 1835 async syncGooglePayButton() { 1836 if (this.ppcp_manager.enabled_google_pay !== 'yes') { 1837 this.removeGooglePayContainer(); 1838 return; 1839 } 1840 if (!this.isPayPalGooglePaySdkReady()) { 1841 console.log('[Google Pay] SDK not available (script not loaded or unsupported)'); 1842 this.removeGooglePayContainer(); 1843 return; 1844 } 1845 const googlePayConfig = await this.getGooglePayConfig(); 1846 const allowedPaymentMethods = googlePayConfig?.allowedPaymentMethods; 1847 if (!allowedPaymentMethods || !allowedPaymentMethods.length) { 1848 console.log('[Google Pay] No allowed payment methods'); 1849 this.removeGooglePayContainer(); 1850 return; 1851 } 1852 const shippingRequired = this.ppcp_manager.needs_shipping === "1"; 1853 const paymentsClient = this.getGooglePaymentsClient({requireOnPaymentDataChanged: shippingRequired}); 1854 try { 1855 const response = await paymentsClient.isReadyToPay(this.getGoogleIsReadyToPayRequest(allowedPaymentMethods)); 1856 if (!response.result) { 1857 console.log('[Google Pay] isReadyToPay returned false'); 1858 this.removeGooglePayContainer(); 1859 return; 1860 } 1861 document.querySelectorAll('.google-pay-container').forEach(container => { 1862 container.innerHTML = ''; 1863 this.renderGooglePayButton(container); 1864 }); 1865 } catch (error) { 1866 console.error('[Google Pay] isReadyToPay failed', error); 1867 this.removeGooglePayContainer(); 1868 } 1869 } 1870 1871 renderGooglePayButton(container) { 1872 if (!container) { 1873 return; 1874 } 1875 const context = container.getAttribute('data-context') || 'product'; 1876 const labelMap = { 1877 product: this.ppcp_manager.google_pay_style_label, 1878 cart: this.ppcp_manager.google_pay_style_label, 1879 checkout: this.ppcp_manager.google_pay_style_label, 1880 express_checkout: this.ppcp_manager.google_pay_express_checkout_style_label, 1881 mini_cart: this.ppcp_manager.google_pay_mini_cart_style_label 1882 }; 1883 const colorMap = { 1884 product: this.ppcp_manager.google_pay_style_color, 1885 cart: this.ppcp_manager.google_pay_style_color, 1886 checkout: this.ppcp_manager.google_pay_style_color, 1887 express_checkout: this.ppcp_manager.google_pay_express_checkout_style_color, 1888 mini_cart: this.ppcp_manager.google_pay_mini_cart_style_color 1889 }; 1890 const shapeMap = { 1891 product: this.ppcp_manager.google_pay_style_shape, 1892 cart: this.ppcp_manager.google_pay_style_shape, 1893 checkout: this.ppcp_manager.google_pay_style_shape, 1894 express_checkout: this.ppcp_manager.google_pay_express_checkout_style_shape, 1895 mini_cart: this.ppcp_manager.google_pay_mini_cart_style_shape 1896 }; 1897 const heightMap = { 1898 product: this.ppcp_manager.button_height, 1899 cart: this.ppcp_manager.button_height, 1900 checkout: this.ppcp_manager.button_height, 1901 express_checkout: this.ppcp_manager.express_checkout_button_height, 1902 mini_cart: this.ppcp_manager.mini_cart_button_height 1903 }; 1904 const buttonType = labelMap[context] || 'plain'; 1905 const buttonColor = colorMap[context] || 'black'; 1906 const buttonShape = shapeMap[context] || 'rect'; 1907 const buttonHeight = parseInt(heightMap[context]) || 40; 1908 let buttonRadius; 1909 if (buttonShape === 'rect') { 1910 buttonRadius = 4; 1911 } else { 1912 buttonRadius = Math.round(buttonHeight / 2); 1913 } 1914 const paymentsClient = this.getGooglePaymentsClient(); 1915 const button = paymentsClient.createButton({ 1916 buttonColor: buttonColor, 1917 buttonType: buttonType, 1918 buttonRadius: buttonRadius, 1919 buttonLocale: this.ppcp_manager.locale, 1920 buttonSizeMode: 'fill', 1921 onClick: this.onGooglePaymentButtonClicked.bind(this) 1922 }); 1923 button.setAttribute('data-context', context); 1924 container.innerHTML = ''; 1925 container.appendChild(button); 1926 button.querySelector('.gpay-button').style.setProperty('border-radius', `${buttonRadius}
1926px`, 'important'); 1927 var $targets = $('.google-pay-container'); 1928 setTimeout(function () { 1929 $targets.css({background: '', 'background-color': ''}); 1930 $targets.each(function () { 1931 this.style.setProperty('--wpg-skel-fallback-bg', 'transparent'); 1932 }); 1933 $targets.addClass('bg-cleared'); 1934 }, 1); 1935 } 1936 1937 async onGooglePaymentButtonClicked(event) { 1938 try { 1939 this.showSpinner(); 1940 const button = event?.target?.closest('button'); 1941 const clickedWrapper = button?.parentElement; 1942 this.pageContext = clickedWrapper?.getAttribute('data-context') || 'unknown'; 1943 const transactionInfo = await this.ppcpGettransactionInfo(); 1944 if (transactionInfo?.success === false) { 1945 const messages = transactionInfo.data?.messages ?? transactionInfo.data ?? ['Unknown error']; 1946 this.showError(messages); 1947 this.hideSpinner(); 1948 throw new Error(messages); 1949 } 1950 if (transactionInfo?.success && transactionInfo.data) { 1951 if (typeof this.setTotalsFromResponse === 'function') { 1952 this.setTotalsFromResponse(transactionInfo.data); 1953 } 1954 } 1955 const shippingRequired = this.ppcp_manager.needs_shipping === "1" && this.pageContext !== 'checkout'; 1956 const paymentsClient = this.getGooglePaymentsClient({requireOnPaymentDataChanged: shippingRequired}); 1957 const paymentDataRequest = await this.getGooglePaymentDataRequest(); 1958 await paymentsClient.loadPaymentData(paymentDataRequest); 1959 } catch (error) { 1960 console.error('[GPay] loadPaymentData error:', error); 1961 this.hideSpinner(); 1962 if (error?.statusCode === "CANCELED") { 1963 window.location.reload(true); 1964 console.warn("Google Pay was cancelled by the user."); 1965 return; 1966 } 1967 console.error("Google Pay Button Click Error:", error); 1968 } 1969 } 1970 1971 getGoogleShippingOptionParameters() { 1972 const methods = Array.isArray(this.ppcp_manager.shipping_methods) ? this.ppcp_manager.shipping_methods : []; 1973 if (!methods.length) { 1974 return null; 1975 } 1976 this.googleShippingIdMap = {}; 1977 const formatPrice = (amount) => { 1978 const n = parseFloat(String(amount || "0").replace(/,/g, "")) || 0; 1979 const currency = this.ppcp_manager.currency || "USD"; 1980 const locale = this.ppcp_manager.locale || "en-US"; 1981 return new Intl.NumberFormat(locale, { 1982 style: "currency", 1983 currency 1984 }).format(n); 1985 }; 1986 let defaultInternalId = this.googleSelectedShippingId; 1987 if (!defaultInternalId) { 1988 const selected = methods.find(m => m.is_selected); 1989 defaultInternalId = selected?.id || methods[0].id; 1990 } 1991 const options = methods.map(method => { 1992 const internalId = method.id; 1993 const safeId = internalId.replace(/[^a-zA-Z0-9 _-]/g, "_"); 1994 this.googleShippingIdMap[safeId] = internalId; 1995 const formattedAmount = formatPrice(method.amount); 1996 return { 1997 id: safeId, 1998 label: method.label || internalId, 1999 description: formattedAmount 2000 }; 2001 }); 2002 let defaultSafeId = options[0].id; 2003 const found = options.find(opt => this.googleShippingIdMap[opt.id] === defaultInternalId); 2004 if (found) defaultSafeId = found.id; 2005 return { 2006 defaultSelectedOptionId: defaultSafeId, 2007 shippingOptions: options 2008 }; 2009 } 2010 2011 getGoogleShippingOptionParametersOrPlaceholder() { 2012 const real = this.getGoogleShippingOptionParameters(); 2013 if (real && Array.isArray(real.shippingOptions) && real.shippingOptions.length > 0) { 2014 return real; 2015 } 2016 return { 2017 defaultSelectedOptionId: 'pending', 2018 shippingOptions: [{ 2019 id: 'pending', 2020 label: this.t('calculating_shipping', 'Shipping'), 2021 description: this.t('calculating_shipping_desc', 'Calculated after address selection') 2022 }] 2023 }; 2024 } 2025 2026 async getGooglePaymentDataRequest() { 2027 const {allowedPaymentMethods, merchantInfo} = await this.getGooglePayConfig(); 2028 const shippingRequired = this.ppcp_manager.needs_shipping === "1" && this.pageContext !== 'checkout'; 2029 const callbackIntents = ['PAYMENT_AUTHORIZATION']; 2030 if (shippingRequired) { 2031 callbackIntents.push('SHIPPING_ADDRESS', 'SHIPPING_OPTION'); 2032 } 2033 const paymentDataRequest = { 2034 apiVersion: this.googlePayApiVersion || 2, 2035 apiVersionMinor: this.googlePayApiVersionMinor || 0, 2036 allowedPaymentMethods, 2037 transactionInfo: this.getGoogleTransactionInfo(), 2038 merchantInfo, 2039 emailRequired: true, 2040 callbackIntents 2041 }; 2042 if (shippingRequired) { 2043 paymentDataRequest.shippingAddressRequired = true; 2044 paymentDataRequest.shippingAddressParameters = { 2045 phoneNumberRequired: true 2046 }; 2047 paymentDataRequest.shippingOptionRequired = true; 2048 paymentDataRequest.shippingOptionParameters = this.getGoogleShippingOptionParametersOrPlaceholder(); 2049 } 2050 return paymentDataRequest; 2051 } 2052 2053 async ppcpGettransactionInfo() { 2054 try { 2055 this.showSpinner(); 2056 $('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove(); 2057 let data = ''; 2058 const isBlockCheckout = this.ppcp_manager.is_block_enable === 'yes'; 2059 switch (this.pageContext) { 2060 case 'checkout': 2061 data = isBlockCheckout ? $('form.wc-block-checkout__form').serialize() : $('form.checkout').serialize(); 2062 if (isBlockCheckout) { 2063 const billingAddress = this.getBillingAddress();
2064 const shippingAddress = this.getShippingAddress(); 2065 data += '&billing_address=' + encodeURIComponent(JSON.stringify(billingAddress)); 2066 data += '&shipping_address=' + encodeURIComponent(JSON.stringify(shippingAddress)); 2067 } 2068 break; 2069 case 'product': 2070 $('<input>', { 2071 type: 'hidden', 2072 name: 'ppcp-add-to-cart', 2073 value: $("[name='add-to-cart']").val() 2074 }).appendTo('form.cart'); 2075 data = $('form.cart').serialize(); 2076 break; 2077 case 'express_checkout': 2078 break; 2079 default: 2080 data = $('form.woocommerce-cart-form').serialize(); 2081 break; 2082 } 2083 // First server call of a wallet purchase, so it is where the nonces get 2084 // brought back in line with the session. The shipping and total endpoints 2085 // the wallet sheet calls next verify ajax_nonce and answer a stale one 2086 // with a 403, which reads to the shopper as a wallet that simply refuses 2087 // to open. 2088 const appendNonce = isBlockCheckout && this.pageContext === 'checkout'; 2089 const nonce = await this.refreshCheckoutNonce(data, appendNonce); 2090 data = this.applyCheckoutNonce(data, nonce, appendNonce); 2091 const transactionInfoUrl = `${this.ppcp_manager.get_transaction_info_url}&form=${encodeURIComponent(this.pageContext)}&used=google_pay`; 2092 const response = await fetch(transactionInfoUrl, { 2093 method: 'POST', 2094 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 2095 body: data 2096 }); 2097 return await response.json(); 2098 } catch (error) { 2099 console.error('Error in ppcpGettransactionInfo:', error); 2100 return null; 2101 } finally { 2102 this.hideSpinner(); 2103 } 2104 } 2105 2106 getGoogleTransactionInfo() { 2107 const currency = this.ppcp_manager?.currency || "USD"; 2108 const country = this.googlePayMerchantCountry || this.ppcp_manager?.country || "US"; 2109 const toNum = (v) => { 2110 const f = parseFloat((v ?? "").toString().replace(/,/g, "")); 2111 return Number.isFinite(f) ? f : 0; 2112 }; 2113 const toStr = (v) => toNum(v).toFixed(2); 2114 const displayItems = []; 2115 const items = Array.isArray(this.ppcp_manager?.cart_items) ? this.ppcp_manager.cart_items : []; 2116 for (const it of items) { 2117 const name = it?.name ?? "Item"; 2118 const qty = Math.max(1, parseInt(it?.quantity, 10) || 1); 2119 const lineTotal = ("subtotal" in it) ? toNum(it.subtotal) : toNum(it.price) * qty; 2120 displayItems.push({ 2121 label: `${name}${qty > 1 ? ` Ã ${qty}` : ""}`, 2122 type: "LINE_ITEM", 2123 price: toStr(lineTotal) 2124 }); 2125 } 2126 const needsShipping = String(this.ppcp_manager?.needs_shipping ?? "0") === "1"; 2127 const shippingTotal = toNum(this.ppcp_manager?.shipping_total); 2128 if (needsShipping && shippingTotal > 0) { 2129 displayItems.push({ 2130 label: "Shipping", 2131 type: "SHIPPING_OPTION", 2132 price: toStr(shippingTotal) 2133 }); 2134 } 2135 const taxTotal = toNum(this.ppcp_manager?.tax_total); 2136 if (taxTotal > 0) { 2137 displayItems.push({ 2138 label: "Tax", 2139 type: "TAX", 2140 price: toStr(taxTotal) 2141 }); 2142 } 2143 const discountTotal = toNum(this.ppcp_manager?.discount_total); 2144 if (discountTotal > 0) { 2145 displayItems.push({ 2146 label: "Discount",
2147 type: "DISCOUNT", 2148 price: toStr(-discountTotal) 2149 }); 2150 } 2151 const total = toNum(this.ppcp_manager?.cart_total); 2152 return { 2153 displayItems, 2154 currencyCode: currency, 2155 countryCode: country, 2156 totalPriceStatus: "ESTIMATED", 2157 totalPrice: toStr(total), 2158 totalPriceLabel: "Total", 2159 checkoutOption: "DEFAULT" 2160 }; 2161 } 2162 2163 async onPaymentAuthorized(paymentData) { 2164 try { 2165 // Always extract wallet addresses so googleapplecreateOrder() can use them 2166 // as a fallback when the Block Checkout form has not been filled. 2167 const billingRaw = paymentData?.paymentMethodData?.info?.billingAddress || {}; 2168 const shippingRaw = paymentData?.shippingAddress || {}; 2169 const email = paymentData?.email || ''; 2170 const billingAddress = { 2171 name: billingRaw.name || '', 2172 surname: '', 2173 address1: billingRaw.address1 || '', 2174 address2: billingRaw.address2 || '', 2175 city: billingRaw.locality || '', 2176 state: billingRaw.administrativeArea || '', 2177 postcode: billingRaw.postalCode || '', 2178 country: billingRaw.countryCode || '', 2179 phoneNumber: billingRaw.phoneNumber || '', 2180 emailAddress: email || '' 2181 }; 2182 const shippingAddress = { 2183 name: shippingRaw.name || '', 2184 surname: '', 2185 address1: shippingRaw.address1 || '', 2186 address2: shippingRaw.address2 || '', 2187 city: shippingRaw.locality || '', 2188 state: shippingRaw.administrativeArea || '', 2189 postcode: shippingRaw.postalCode || '', 2190 country: shippingRaw.countryCode || '', 2191 phoneNumber: shippingRaw.phoneNumber || '' 2192 }; 2193 this.walletBillingAddress = billingAddress; 2194 this.walletShippingAddress = shippingAddress; 2195 if (this.pageContext !== 'checkout') { 2196 await this.fetchUpdatedTotalFromBackend(shippingAddress, billingAddress); 2197 } 2198 if (this.googleSelectedShippingId === 'pending' || !this.googleSelectedShippingId) { 2199 const methods = Array.isArray(this.ppcp_manager.shipping_methods) ? this.ppcp_manager.shipping_methods : []; 2200 if (methods.length) { 2201 const pick = methods.find(m => m.is_selected) || methods[0]; 2202 this.googleSelectedShippingId = pick.id; 2203 } 2204 } 2205 const orderId = await this.googleapplecreateOrder(); 2206 if (!orderId) { 2207 throw new Error("Order creation failed."); 2208 } 2209 const result = await wpg_paypal_sdk.Googlepay().confirmOrder({ 2210 orderId, 2211 paymentMethodData: paymentData.paymentMethodData 2212 }).catch(err => { 2213 // The SDK rejects with internal error names (e.g. 2214 // APPROVE_GOOGLE_PAY_VALIDATION_ERROR); log those for debugging but 2215 // show the shopper the translated message instead. 2216 console.error('[Google Pay] confirmOrder error:', err); 2217 throw new Error(this.t('google_pay_not_approved', 'Google Pay payment could not be approved. Please try again or use a different payment method.')); 2218 }); 2219 // approveGooglePayPayment reports the outcome via the order status. 2220 // Depending on the SDK build it is returned at the top level or nested 2221 // under approveGooglePayPayment. Only APPROVED (or PAYER_ACTION_REQUIRED, 2222 // once the payer completes the required action) may proceed to capture; 2223 // any other value means PayPal rejected the token â e.g. an undecryptable 2224 // Google Pay token (INVALID_GOOGLE_PAY_TOKEN) â and must not be captured 2225 // or reported as a success. This mirrors the Apple Pay confirmOrder guard. 2226 const status = result?.status ?? result?.approveGooglePayPayment?.status ?? null; 2227 if (status === "PAYER_ACTION_REQUIRED") { 2228 await wpg_paypal_sdk.Googlepay().initiatePayerAction({ orderId }); 2229 } else if (status !== "APPROVED") { 2230 throw new Error(this.t('google_pay_not_approved', 'Google Pay payment could not be approved. Please try again or use a different payment method.'));
2231 } 2232 this.onApproveHandler({ orderID: orderId }, 'google_pay'); 2233 return { transactionState: "SUCCESS" }; 2234 } catch (error) { 2235 this.showError(error.message || "Google Pay failed."); 2236 this.hideSpinner(); 2237 return { 2238 transactionState: "ERROR", 2239 error: { 2240 intent: "PAYMENT_AUTHORIZATION", 2241 message: error.message || "Google Pay failed." 2242 } 2243 }; 2244 } 2245 } 2246 2247 setTotalsFromResponse(d = {}) { 2248 if (!d) { 2249 return; 2250 } 2251 2252 this.ppcp_manager.cart_total = d.total ?? d.cart_total ?? this.ppcp_manager.cart_total; 2253 this.ppcp_manager.cart_items = Array.isArray(d.cart_items) ? d.cart_items : (this.ppcp_manager.cart_items || []); 2254 this.ppcp_manager.shipping_total = d.shipping_total ?? this.ppcp_manager.shipping_total ?? "0.00"; 2255 this.ppcp_manager.tax_total = d.tax_total ?? this.ppcp_manager.tax_total ?? "0.00"; 2256 this.ppcp_manager.discount_total = d.discount_total ?? this.ppcp_manager.discount_total ?? "0.00"; 2257 this.ppcp_manager.currency = d.currency ?? this.ppcp_manager.currency; 2258 this.ppcp_manager.needs_shipping = d.needs_shipping ?? this.ppcp_manager.needs_shipping ?? "0"; 2259 this.ppcp_manager.shipping_methods = Array.isArray(d.shipping_methods) 2260 ? d.shipping_methods 2261 : (this.ppcp_manager.shipping_methods || []); 2262 2263 const selected = (this.ppcp_manager.shipping_methods || []).find(m => m.is_selected); 2264 if (selected && selected.id) { 2265 this.googleSelectedShippingId = selected.id; 2266 this.appleSelectedShippingId = selected.id; 2267 } 2268 } 2269 2270 2271 async validateShippingAddressFromBackend(shippingAddress, selectedShippingId = '') { 2272 const params = new URLSearchParams({ 2273 action: 'ppcp_validate_shipping_address', 2274 security: this.ppcp_manager.ajax_nonce, 2275 shipping_address: JSON.stringify(shippingAddress || {}) 2276 }); 2277 if (selectedShippingId) { 2278 params.append('selected_shipping_id', selectedShippingId); 2279 } 2280 const res = await fetch(this.ppcp_manager.ajax_url, { 2281 method: 'POST', 2282 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 2283 body: params 2284 }); 2285 // admin-ajax.php always returns 200, so parse body 2286 const result = await res.json().catch(() => null); 2287 return result; 2288 } 2289 2290 async fetchUpdatedTotalFromBackend(shippingAddress, billingAddress = null, selectedShippingId = '') { 2291 try { 2292 const params = new URLSearchParams({ 2293 action: 'ppcp_get_updated_total', 2294 security: this.ppcp_manager.ajax_nonce, 2295 shipping_address: JSON.stringify(shippingAddress || {}), 2296 billing_address: billingAddress ? JSON.stringify(billingAddress) : '', 2297 context: this.pageContext 2298 }); 2299 if (selectedShippingId) { 2300 params.append('selected_shipping_id', selectedShippingId); 2301 } 2302 const res = await fetch(this.ppcp_manager.ajax_url, { 2303 method: 'POST', 2304 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 2305 body: params 2306 }); 2307 const result = await res.json(); 2308 if (result?.success && result.data) { 2309 if (typeof this.setTotalsFromResponse === 'function') { 2310 this.setTotalsFromResponse(result.data); 2311 }
2312 if (result.data.total != null) { 2313 const n = parseFloat(String(result.data.total).replace(/,/g, '')); 2314 return Number.isFinite(n) ? n.toFixed(2) : String(result.data.total); 2315 } 2316 } 2317 const n = parseFloat(String(this.ppcp_manager.cart_total || '0').replace(/,/g, '')); 2318 return Number.isFinite(n) ? n.toFixed(2) : (this.ppcp_manager.cart_total || '0.00'); 2319 } catch (e) { 2320 console.error('Error fetching updated total:', e); 2321 const n = parseFloat(String(this.ppcp_manager.cart_total || '0').replace(/,/g, '')); 2322 return Number.isFinite(n) ? n.toFixed(2) : (this.ppcp_manager.cart_total || '0.00'); 2323 } 2324 } 2325 2326 formatAmount(amount) { 2327 if (typeof amount === 'number') { 2328 return amount.toFixed(2); 2329 } 2330 if (typeof amount === 'string') { 2331 const parsed = parseFloat(amount); 2332 return isNaN(parsed) ? "0.00" : parsed.toFixed(2); 2333 } 2334 return "0.00"; 2335 } 2336 2337 prefetchProductTotal() { 2338 const hasProductContainer = document.querySelector('#ppcp_product, .google-pay-container[data-context="product"], .apple-pay-container[data-context="product"]'); 2339 if (!hasProductContainer) { 2340 const hasApplePayContainer = document.querySelector('.apple-pay-container'); 2341 if (hasApplePayContainer) { 2342 this.fetchProductTotal(0, 0); 2343 } 2344 return; 2345 } 2346 const getCurrentProductId = () => { 2347 const variationInput = document.querySelector('input.variation_id'); 2348 const variationId = parseInt(variationInput?.value || 0, 10); 2349 return variationId > 0 2350 ? variationId 2351 : parseInt(this.ppcp_manager?.product_id || 0, 10); 2352 }; 2353 const getQty = () => parseFloat(document.querySelector('input.qty')?.value || '1') || 1; 2354 this.fetchProductTotal(getCurrentProductId(), getQty()); 2355 } 2356 2357 fetchProductTotal(productId, quantity = 1) { 2358 const data = new URLSearchParams({ 2359 action: 'ppcp_get_product_total', 2360 security: this.ppcp_manager.ajax_nonce, 2361 product_id: productId, 2362 quantity: quantity 2363 }); 2364 2365 // Product-option plugins (Product Add-ons, TM Extra Product Options, 2366 // Advanced Product Fields) maintain hidden cost fields on form.cart; 2367 // the server-side wpg_ppcp_product_total filter reads them from this 2368 // request, so the wallet-sheet total includes the option costs. 2369 ['addon_costs', 'tm_epo_costs', 'wapf_costs'].forEach((field) => { 2370 const input = document.querySelector(`form.cart input[name="${field}"]`); 2371 if (input && input.value !== '') { 2372 data.append(field, input.value); 2373 } 2374 }); 2375 2376 fetch(this.ppcp_manager.ajax_url, { 2377 method: 'POST', 2378 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 2379 body: data.toString() 2380 }).then(res => res.json()) 2381 .then(response => { 2382 if (response.success && response.data?.combined_total) { 2383 this.ppcp_manager.cart_total = response.data.combined_total; 2384 } 2385 }); 2386 } 2387 2388 removeApplePayContainer() { 2389 setTimeout(() => { 2390 const containers = document.querySelectorAll('.apple-pay-container'); 2391 containers.forEach(container => { 2392 container.remove(); 2393 }); 2394 }, 500); 2395 } 2396 2397 async syncApplePayButton() { 2398 if (this.ppcp_manager.enabled_apple_pay !== 'yes') { 2399 this.removeApplePayContainer(); 2400 return; 2401 } 2402 if (window.location.protocol !== 'https:') { 2403 console.log("Apple Pay requires HTTPS. Current protocol:", window.location.protocol); 2404 this.removeApplePayContainer(); 2405 return; 2406 } 2407 if (!window.ApplePaySession) { 2408 console.log("Apple Pay is not supported on this device."); 2409 this.removeApplePayContainer(); 2410 return; 2411 } 2412 if (!ApplePaySession.canMakePayments()) { 2413 console.log("Apple Pay cannot make payments on this device."); 2414 this.removeApplePayContainer(); 2415 return; 2416 } 2417 if (typeof wpg_paypal_sdk === 'undefined' || typeof wpg_paypal_sdk.Applepay !== 'function') { 2418 this.removeApplePayContainer(); 2419 return; 2420 } 2421 try { 2422 const applepay = wpg_paypal_sdk.Applepay(); 2423 const config = await applepay.config({ environment: this.ppcp_manager.environment || "TEST" }); 2424 if (config && config.isEligible) { 2425 this.renderApplePayButton(); 2426 } else { 2427 console.log("Apple Pay is not eligible for this configuration."); 2428 this.removeApplePayContainer(); 2429 } 2430 } catch (error) { 2431 console.error("Failed to load Apple Pay configuration:", error); 2432 this.removeApplePayContainer(); 2433 } 2434 } 2435 2436 renderApplePayButton() { 2437 const containers = document.querySelectorAll(".apple-pay-container"); 2438 if (containers.length === 0) { 2439 return; 2440 } 2441 this.prefetchProductTotal();
2442 containers.forEach(container => { 2443 container.innerHTML = ''; 2444 const applePayButton = document.createElement('apple-pay-button'); 2445 const context = container.getAttribute('data-context') || 'product'; 2446 const labelMap = { 2447 product: this.ppcp_manager.apple_pay_style_label, 2448 cart: this.ppcp_manager.apple_pay_style_label, 2449 checkout: this.ppcp_manager.apple_pay_style_label, 2450 express_checkout: this.ppcp_manager.apple_pay_express_checkout_style_label, 2451 mini_cart: this.ppcp_manager.apple_pay_mini_cart_style_label 2452 }; 2453 const colorMap = { 2454 product: this.ppcp_manager.apple_pay_style_color, 2455 cart: this.ppcp_manager.apple_pay_style_color, 2456 checkout: this.ppcp_manager.apple_pay_style_color, 2457 express_checkout: this.ppcp_manager.apple_pay_express_checkout_style_color, 2458 mini_cart: this.ppcp_manager.apple_pay_mini_cart_style_color 2459 }; 2460 const heightMap = { 2461 product: this.ppcp_manager.button_height, 2462 cart: this.ppcp_manager.button_height, 2463 checkout: this.ppcp_manager.button_height, 2464 express_checkout: this.ppcp_manager.express_checkout_button_height, 2465 mini_cart: this.ppcp_manager.mini_cart_button_height 2466 }; 2467 const shapeMap = { 2468 product: this.ppcp_manager.apple_pay_style_shape, 2469 cart: this.ppcp_manager.apple_pay_style_shape, 2470 checkout: this.ppcp_manager.apple_pay_style_shape, 2471 express_checkout: this.ppcp_manager.apple_pay_express_checkout_style_shape, 2472 mini_cart: this.ppcp_manager.apple_pay_mini_cart_style_shape 2473 }; 2474 const buttonType = labelMap[context] || 'plain'; 2475 const buttonColor = colorMap[context] || 'black'; 2476 const buttonHeight = parseInt(heightMap[context]) || 40; 2477 const buttonShape = shapeMap[context] || 'rect'; 2478 const buttonRadius = buttonShape === 'pill' ? 20 : 4; 2479 2480 container.classList.add(buttonShape === 'pill' ? 'apple-shape-pill' : 'apple-shape-rect'); 2481 2482 container.style.setProperty('--button-height', `${buttonHeight}px`); 2483 container.style.setProperty('--button-radius', `${buttonRadius}px`); 2484 container.style.height = `${buttonHeight}px`; 2485 2486 applePayButton.setAttribute('type', buttonType); 2487 applePayButton.setAttribute('buttonstyle', buttonColor); 2488 applePayButton.setAttribute('data-context', context); 2489 container.appendChild(applePayButton); 2490 applePayButton.addEventListener('click', () => this.onApplePayButtonClicked(container)); 2491 var $targets = $('.apple-pay-container'); 2492 setTimeout(function () { 2493 $targets.css({background: '', 'background-color': ''}); 2494 $targets.each(function () { 2495 this.style.setProperty('--wpg-skel-fallback-bg', 'transparent'); 2496 }); 2497 $targets.addClass('bg-cleared'); 2498 }, 1); 2499 }); 2500 } 2501 2502 getAppleShippingMethods() { 2503 const methods = Array.isArray(this.ppcp_manager.shipping_methods) ? this.ppcp_manager.shipping_methods : []; 2504 if (!methods.length) { 2505 return []; 2506 } 2507 this.appleShippingIdMap = {}; 2508 let defaultInternalId = this.appleSelectedShippingId; 2509 if (!defaultInternalId) { 2510 const selected = methods.find(m => m.is_selected); 2511 defaultInternalId = (selected && selected.id) ? selected.id : methods[0].id; 2512 } 2513 this.appleSelectedShippingId = defaultInternalId; 2514 2515 const appleMethods = methods.map(method => { 2516 const internalId = method.id; 2517 const safeId = internalId.replace(/[^a-zA-Z0-9 _-]/g, "_"); 2518 this.appleShippingIdMap[safeId] = internalId; 2519 2520 return { 2521 label: method.label || internalId,
2522 amount: this.formatAmount(method.amount), 2523 identifier: safeId, 2524 detail: method.description || '' 2525 }; 2526 }); 2527 return appleMethods; 2528 } 2529 2530 onApplePayButtonClicked(container) { 2531 try { 2532 this.showSpinner(); 2533 this.pageContext = container?.getAttribute('data-context') || 'unknown'; 2534 if (!window.ApplePaySession || !ApplePaySession.canMakePayments()) { 2535 console.warn('Apple Pay is not available on this device or browser.'); 2536 this.hideSpinner(); 2537 return; 2538 } 2539 const applepay = wpg_paypal_sdk.Applepay(); 2540 const needsShipping = this.ppcp_manager.needs_shipping === "1" && this.pageContext !== 'checkout'; 2541 const paymentRequest = { 2542 countryCode: this.ppcp_manager.country || "US", 2543 currencyCode: this.ppcp_manager.currency || "USD", 2544 supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'], 2545 merchantCapabilities: ['supports3DS'], 2546 requiredBillingContactFields: ["postalAddress", "email", "phone", "name"], 2547 total: { 2548 label: this.ppcp_manager.store_label || "Total",
2549 amount: this.formatAmount(this.ppcp_manager.cart_total), 2550 type: "final" 2551 } 2552 }; 2553 if (needsShipping) { 2554 paymentRequest.requiredShippingContactFields = ["postalAddress", "name", "phone", "email"]; 2555 const methods = this.getAppleShippingMethods(); 2556 if (methods.length) { 2557 paymentRequest.shippingMethods = methods; 2558 } else { 2559 paymentRequest.shippingMethods = [{ 2560 label: 'Shipping', 2561 amount: '0.00', 2562 identifier: 'pending', 2563 detail: 'Calculated after address selection' 2564 }]; 2565 } 2566 } else { 2567 paymentRequest.requiredShippingContactFields = ["name", "phone", "email"]; 2568 } 2569 2570 const session = new ApplePaySession(4, paymentRequest); 2571 session.onvalidatemerchant = async (event) => { 2572 try { 2573 const validation = await applepay.validateMerchant({ 2574 validationUrl: event.validationURL, 2575 displayName: this.ppcp_manager.store_label || "Store" 2576 }); 2577 session.completeMerchantValidation(validation.merchantSession); 2578 } catch (err) { 2579 console.error('Merchant validation failed:', err); 2580 session.abort(); 2581 this.hideSpinner(); 2582 } 2583 }; 2584 2585 if (needsShipping) { 2586 session.onshippingcontactselected = async (event) => { 2587 try { 2588 if (this.pageContext === 'product') { 2589 const transactionInfo = await this.ppcpGettransactionInfo(); 2590 if (transactionInfo?.success === false) { 2591 const messages = transactionInfo.data?.messages ?? transactionInfo.data ?? ['Unknown error']; 2592 this.showError(messages); 2593 throw new Error(messages); 2594 session.completePayment({ 2595 status: ApplePaySession.STATUS_FAILURE 2596 }); 2597 } 2598 if (transactionInfo?.success && transactionInfo.data && typeof this.setTotalsFromResponse === 'function') { 2599 this.setTotalsFromResponse(transactionInfo.data); 2600 } 2601 } 2602 2603 const shipping = event.shippingContact; 2604 if (!shipping || !shipping.countryCode || !shipping.postalCode) { 2605 throw new Error(this.t('shipping_address_incomplete', 'Shipping address is incomplete.')); 2606 } 2607 2608 // â Validate shipping country/address via the same AJAX endpoint used by PayPal onShippingChange 2609 const vr = await this.validateShippingAddressFromBackend({ 2610 city: shipping.locality || '', 2611 state: shipping.administrativeArea || '', 2612 countryCode: shipping.countryCode || '', 2613 postalCode: shipping.postalCode || '' 2614 }); 2615 if (!vr || vr.success !== true) { 2616 const msg = (vr && vr.data) ? String(vr.data) : this.t('shipping_unserviceable', 'We do not ship to this address.'); 2617 const errors = []; 2618 if (window.ApplePayError) { 2619 errors.push(new ApplePayError('shippingContactInvalid', 'postalAddress', msg)); 2620 } 2621 session.completeShippingContactSelection({ 2622 errors, 2623 newTotal: { 2624 label: this.ppcp_manager.store_label || "Store",
2625 amount: this.formatAmount(this.ppcp_manager.cart_total), 2626 type: "final" 2627 } 2628 }); 2629 return; 2630 } 2631 2632 const updatedTotal = await this.fetchUpdatedTotalFromBackend({ 2633 city: shipping.locality || '', 2634 state: shipping.administrativeArea || '', 2635 postcode: shipping.postalCode || '', 2636 country: shipping.countryCode || '' 2637 }); 2638 2639 const methods = this.getAppleShippingMethods(); 2640 const update = { 2641 newTotal: { 2642 label: this.ppcp_manager.store_label || "Store", 2643 amount: updatedTotal, 2644 type: "final" 2645 } 2646 }; 2647 2648 if (methods.length) { 2649 update.newShippingMethods = methods; 2650 } 2651 2652 session.completeShippingContactSelection(update); 2653 } catch (error) { 2654 console.error('[ApplePay] onshippingcontactselected error:', error); 2655 const fallbackMethods = this.getAppleShippingMethods(); 2656 const update = { 2657 newTotal: { 2658 label: this.ppcp_manager.store_label || "Store",
2659 amount: this.formatAmount(this.ppcp_manager.cart_total), 2660 type: "final" 2661 } 2662 }; 2663 if (fallbackMethods.length) { 2664 update.newShippingMethods = fallbackMethods; 2665 } 2666 session.completeShippingContactSelection(update); 2667 } 2668 }; 2669 2670 session.onshippingmethodselected = async (event) => { 2671 try { 2672 const method = event.shippingMethod; 2673 if (!method || !method.identifier) { 2674 throw new Error(this.t('invalid_shipping_method', 'Invalid shipping method.')); 2675 } 2676 if (method.identifier === 'pending') { 2677 session.completeShippingMethodSelection({ 2678 newTotal: { 2679 label: this.ppcp_manager.store_label || "Store",
2680 amount: this.formatAmount(this.ppcp_manager.cart_total), 2681 type: "final" 2682 } 2683 }); 2684 return; 2685 } 2686 const internalId = this.appleShippingIdMap[method.identifier] || method.identifier; 2687 this.appleSelectedShippingId = internalId; 2688 2689 const updatedTotal = await this.fetchUpdatedTotalFromBackend( 2690 {}, // shipping address unchanged 2691 null, // billing not needed here 2692 internalId 2693 ); 2694 2695 const update = { 2696 newTotal: { 2697 label: this.ppcp_manager.store_label || "Store", 2698 amount: updatedTotal, 2699 type: "final" 2700 } 2701 }; 2702 2703 session.completeShippingMethodSelection(update); 2704 } catch (error) { 2705 console.error('[ApplePay] onshippingmethodselected error:', error); 2706 session.completeShippingMethodSelection({ 2707 newTotal: { 2708 label: this.ppcp_manager.store_label || "Store",
2709 amount: this.formatAmount(this.ppcp_manager.cart_total), 2710 type: "final" 2711 } 2712 }); 2713 } 2714 }; 2715 } 2716 2717 session.onpaymentauthorized = async (event) => { 2718 try { 2719 2720 const transactionInfo = await this.ppcpGettransactionInfo(); 2721 if (transactionInfo.success === false) { 2722 const messages = transactionInfo.data?.messages ?? transactionInfo.data ?? ['Unknown error']; 2723 this.showError(messages); 2724 throw new Error(messages); 2725 session.completePayment({ 2726 status: ApplePaySession.STATUS_FAILURE 2727 }); 2728 } 2729 if (transactionInfo?.success) { 2730 this.ppcp_manager.cart_total = transactionInfo.data?.cart_total || this.ppcp_manager.cart_total; 2731 } 2732 2733 2734 const billingRaw = event.payment?.billingContact || {}; 2735 const shippingRaw = event.payment?.shippingContact || {}; 2736 const emailAddress = billingRaw.emailAddress || shippingRaw.emailAddress || billingRaw.email || shippingRaw.email || ''; 2737 const billingAddress = { 2738 name: billingRaw.givenName || '', 2739 surname: billingRaw.familyName || '', 2740 address1: billingRaw.addressLines?.[0] || '', 2741 address2: billingRaw.addressLines?.[1] || '', 2742 city: billingRaw.locality || '', 2743 state: billingRaw.administrativeArea || '', 2744 postcode: billingRaw.postalCode || '', 2745 country: billingRaw.countryCode || '', 2746 phoneNumber: billingRaw.phoneNumber || '', 2747 emailAddress: emailAddress 2748 }; 2749 const shippingAddress = { 2750 name: shippingRaw.givenName || '', 2751 surname: shippingRaw.familyName || '', 2752 address1: shippingRaw.addressLines?.[0] || '', 2753 address2: shippingRaw.addressLines?.[1] || '', 2754 city: shippingRaw.locality || '', 2755 state: shippingRaw.administrativeArea || '', 2756 postcode: shippingRaw.postalCode || '', 2757 country: shippingRaw.countryCode || '', 2758 phoneNumber: shippingRaw.phoneNumber || '' 2759 }; 2760 // Store wallet addresses so googleapplecreateOrder() can fall back to 2761 // them when the Block Checkout form is empty. 2762 this.walletBillingAddress = billingAddress; 2763 this.walletShippingAddress = shippingAddress; 2764 2765 if (needsShipping) { 2766 await this.fetchUpdatedTotalFromBackend( 2767 shippingAddress, 2768 billingAddress, 2769 this.appleSelectedShippingId || '' 2770 ); 2771 } 2772 2773 const orderId = await this.googleapplecreateOrder(); 2774 if (!orderId) { 2775 throw new Error("Order creation failed."); 2776 } 2777 2778 const result = await applepay.confirmOrder({ 2779 orderId: orderId, 2780 token: event.payment.token, 2781 billingContact: event.payment.billingContact 2782 }); 2783 2784 const status = result?.approveApplePayPayment?.status; 2785 if (status === "APPROVED") { 2786 this.showSpinner(); 2787 const order_id = orderId; 2788 2789 if (this.isCheckoutPage() || this.ppcp_manager.skip_order_review === 'yes') { 2790 const url = `${this.ppcp_manager.cc_capture}&paypal_order_id=${encodeURIComponent(order_id)}&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`; 2791 $.post(url, (response) => { 2792 if (response?.data?.redirect) { 2793 session.completePayment({ 2794 status: ApplePaySession.STATUS_SUCCESS 2795 }); 2796 window.location.href = response.data.redirect; 2797 return; 2798 }
2799 // Non-redirect response (failure or unexpected/empty body): 2800 // close the Apple Pay sheet and surface an error instead of 2801 // leaving it spinning. 2802 session.completePayment({ 2803 status: ApplePaySession.STATUS_FAILURE 2804 }); 2805 this.hideSpinner(); 2806 const messages = response?.data?.messages ?? [this.t('unknown_error_short', 'An unknown error occurred.')]; 2807 this.showError(messages); 2808 if (response?.success === false) { 2809 let redirectUrl = `${this.ppcp_manager.checkout_url}?paypal_order_id=${encodeURIComponent(order_id)}&from=${this.ppcp_manager.page}`; 2810 window.location.href = redirectUrl; 2811 } 2812 }).fail(() => { 2813 session.completePayment({ 2814 status: ApplePaySession.STATUS_FAILURE 2815 }); 2816 this.hideSpinner(); 2817 this.showError('We could not confirm your payment. If your account was charged, please contact us before trying again.'); 2818 }); 2819 } else { 2820 session.completePayment({ 2821 status: ApplePaySession.STATUS_SUCCESS 2822 }); 2823 let redirectUrl = `${this.ppcp_manager.checkout_url}?paypal_order_id=${encodeURIComponent(order_id)}&from=${this.ppcp_manager.page}`; 2824 window.location.href = redirectUrl; 2825 this.hideSpinner(); 2826 } 2827 } else { 2828 throw new Error(this.t('apple_pay_not_approved', 'Apple Pay confirmation returned non-APPROVED status.')); 2829 } 2830 } catch (error) { 2831 session.completePayment({ 2832 status: ApplePaySession.STATUS_FAILURE 2833 }); 2834 this.showError(error?.message || String(error) || 'An unknown error occurred.'); 2835 this.hideSpinner(); 2836 } 2837 }; 2838 2839 session.oncancel = () => { 2840 window.location.reload(true); 2841 console.log("Apple Pay session cancelled."); 2842 this.hideSpinner(); 2843 }; 2844 session.begin(); 2845 } catch (err) { 2846 console.error('Apple Pay session initialization failed:', err); 2847 this.showError(this.t('apple_pay_init_failed', 'Apple Pay could not be initialized.')); 2848 this.hideSpinner(); 2849 } 2850 } 2851 } 2852 $(function () { 2853 $('.woocommerce #payment #place_order, .woocommerce-page #payment #place_order').hide(); 2854 window.PPCPManager = PPCPManager; 2855 const ppcp_manager = window.ppcp_manager || {}; 2856 window.ppcpManagerInstance = new PPCPManager(ppcp_manager); 2857 }); 2858})(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.