PageSourceSearch

https://www.grimnetwork.it/wp/wp-content/plugins/woo-paypal-gatewa…heckout-for-woocommerce-public.js?ver=9.2.7

js grimnetwork.it collected 2026-10-07 04:23:26 UTC 152,882 bytes, 2,858 lines download raw bytes

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.