(function ($) {
class PPCPManager {
constructor(ppcp_manager) {
// 9.0.66
this.ppcp_manager = ppcp_manager;
this.productAddToCart = true;
this.lastApiResponse = null;
this.ppcp_address = [];
this.paymentsClient = null;
this.allowedPaymentMethods = [];
this.merchantInfo = null;
this.pageContext = 'unknown';
this.ppcp_used_payment_method = null;
this.googleSelectedShippingId = '';
this.googleShippingIdMap = {};
this.appleSelectedShippingId = '';
this.appleShippingIdMap = {};
this.init();
this.ppcp_cart_css();
}
init() {
if (typeof this.ppcp_manager === 'undefined') {
console.log("PPCP Manager configuration is undefined.");
return false;
}
const lastError = (ppcp_manager?.last_error || '').trim();
if (lastError) {
console.log(lastError);
$(document.body).trigger('ppcp_checkout_error', lastError);
}
this.debouncedTogglePlaceOrderButton = this.debounce_place_order(this.togglePlaceOrderButton.bind(this), 4);
this.manageVariations('#ppcp_product, .google-pay-container, .apple-pay-container');
this.manageSubscriptionOptions();
this.prefetchProductTotal();
this.bindCheckoutEvents();
if (this.ppcp_manager.advanced_card_payments === 'yes') {
this.debouncedUpdatePaypalCC = this.debounce_cc(this.syncPayPalCardFields.bind(this), 500);
}
this.debouncedUpdatePaypalCheckout = this.debounce(this.syncPayPalCheckoutButtons.bind(this), 500);
if (this.ppcp_manager.enabled_google_pay === 'yes') {
this.debouncedUpdateGooglePay = this.debounce_google(this.syncGooglePayButton.bind(this), 500);
}
if (this.ppcp_manager.enabled_apple_pay === 'yes') {
this.debouncedUpdateApplePay = this.debounce_apple(this.syncApplePayButton.bind(this), 500);
}
if (this.isCheckoutPage() === false) {
this.debouncedUpdatePaypalCheckout();
if (this.ppcp_manager.enabled_google_pay === 'yes') {
this.debouncedUpdateGooglePay();
}
if (this.ppcp_manager.enabled_apple_pay === 'yes') {
this.debouncedUpdateApplePay();
}
} else {
this.debouncedUpdatePaypalCheckout();
if (this.ppcp_manager.enabled_google_pay === 'yes') {
this.debouncedUpdateGooglePay();
}
if (this.ppcp_manager.enabled_apple_pay === 'yes') {
this.debouncedUpdateApplePay();
}
if (this.ppcp_manager.advanced_card_payments === 'yes') {
this.debouncedUpdatePaypalCC();
}
}
setTimeout(function () {
$('#wfacp_smart_buttons.wfacp-dynamic-checkout-loading .dynamic-checkout__skeleton').hide();
}, 1000);
}
t(key, fallback = '') {
const raw = this.ppcp_manager ? this.ppcp_manager[key] : undefined;
if (raw === null || raw === undefined || raw === '') {
return fallback;
}
const v = String(raw);
return (v && v !== 'undefined' && v !== 'null') ? v : fallback;
}
getAddress(prefix) {
const fields = {
addressLine1: jQuery(`#${prefix}_address_1`).val(),
addressLine2: jQuery(`#${prefix}_address_2`).val(),
adminArea1: jQuery(`#${prefix}_state`).val(),
adminArea2: jQuery(`#${prefix}_city`).val(),
postalCode: jQuery(`#${prefix}_postcode`).val(),
countryCode: jQuery(`#${prefix}_country`).val(),
firstName: jQuery(`#${prefix}_first_name`).val(),
lastName: jQuery(`#${prefix}_last_name`).val(),
email: jQuery(`#${prefix}_email`).val()
};
fields.phoneNumber = prefix === 'billing' ?
jQuery('#billing-phone').val() || jQuery('#shipping-phone').val() :
jQuery('#shipping-phone').val() || jQuery('#billing-phone').val();
let customerData = {};
let addressData = {};
if (!fields.addressLine1) {
if (typeof wp !== 'undefined' && wp.data?.select) {
try {
customerData = wp.data.select('wc/store/cart').getCustomerData();
} catch (e) {
console.warn('Could not fetch customerData:', e);
}
}
const {billingAddress, shippingAddress} = customerData;
addressData = (prefix === 'billing') ? billingAddress : shippingAddress;
Object.assign(fields, {
addressLine1: addressData.address_1,
addressLine2: addressData.address_2,
adminArea1: addressData.state,
adminArea2: addressData.city,
postalCode: addressData.postcode,
countryCode: addressData.country,
firstName: addressData.first_name,
lastName: addressData.last_name,
email: prefix === 'billing' ? billingAddress.email || shippingAddress.email : shippingAddress.email || billingAddress.email
});
}
// Start with the standard fields
const result = {
[`${prefix}_address_1`]: fields.addressLine1 || '',
[`${prefix}_address_2`]: fields.addressLine2 || '',
[`${prefix}_state`]: fields.adminArea1 || '',
[`${prefix}_city`]: fields.adminArea2 || '',
[`${prefix}_postcode`]: fields.postalCode || '',
[`${prefix}_country`]: fields.countryCode || '',
[`${prefix}_first_name`]: fields.firstName || '',
[`${prefix}_last_name`]: fields.lastName || '',
[`${prefix}_email`]: fields.email || '',
[`${prefix}_phone`]: fields.phoneNumber || ''
};
// Add ALL other fields from addressData (including custom fields)
if (addressData && Object.keys(addressData).length > 0) {
Object.keys(addressData).forEach(key => {
const fieldKey = `${prefix}_${key}`;
// Only add if it's not already in our standard fields
if (!result.hasOwnProperty(fieldKey)) {
result[fieldKey] = addressData[key] !== undefined && addressData[key] !== null
? addressData[key]
: '';
}
});
}
return result;
}
getValidAddress(prefix) {
const address = this.getAddress(prefix);
if (this.isValidAddress(prefix, address)) {
return address;
}
// Fall back to the other address (e.g. when "Use same address for billing"
// is checked on Block Checkout the billing address is empty in the store).
// Re-key the fallback to the requested prefix, otherwise it is posted with
// the wrong field names (shipping_* instead of billing_*) and the resulting
// WooCommerce order is created without a billing address.
const otherPrefix = prefix === 'billing' ? 'shipping' : 'billing';
const otherAddress = this.getAddress(otherPrefix);
if (this.isValidAddress(otherPrefix, otherAddress)) {
return this.rekeyAddress(otherAddress, otherPrefix, prefix);
}
return address;
}
// Re-map an address object's field keys from one prefix to another, e.g.
// { shipping_address_1: '...' } -> { billing_address_1: '...' }.
rekeyAddress(address, fromPrefix, toPrefix) {
if (!address) {
return address;
}
const result = {};
const search = `${fromPrefix}_`;
Object.keys(address).forEach(key => {
if (key.indexOf(search) === 0) {
result[`${toPrefix}_${key.substring(search.length)}`] = address[key];
} else {
result[key] = address[key];
}
});
return result;
}
getBillingAddress() {
return this.getValidAddress('billing');
}
getShippingAddress() {
return this.getValidAddress('shipping');
}
isValidAddress(prefix, address) {
return address && address[`${prefix}_address_1`];
}
// Convert a Google Pay / Apple Pay wallet address to WooCommerce form-field format.
// Used as a fallback when the Block Checkout form has not been filled in
// before the customer clicks the express wallet button. Apple Pay populates
// both `name` (given) and `surname` (family); Google Pay puts the full name
// in `name` only.
walletToWCFormat(walletAddr, prefix) {
if (!walletAddr) {
return {};
}
const rawName = (walletAddr.name || '').trim();
const rawSurname = (walletAddr.surname || '').trim();
let firstName = '';
let lastName = '';
if (rawName && rawSurname) {
firstName = rawName;
lastName = rawSurname;
} else if (rawName) {
const parts = rawName.split(/\s+/);
firstName = parts.shift() || '';
lastName = parts.join(' ');
} else if (rawSurname) {
lastName = rawSurname;
}
const result = {
[`${prefix}_first_name`]: firstName,
[`${prefix}_last_name`]: lastName,
[`${prefix}_address_1`]: walletAddr.address1 || '',
[`${prefix}_address_2`]: walletAddr.address2 || '',
[`${prefix}_city`]: walletAddr.city || '',
[`${prefix}_state`]: walletAddr.state || '',
[`${prefix}_postcode`]: walletAddr.postcode || '',
[`${prefix}_country`]: walletAddr.country || '',
[`${prefix}_phone`]: walletAddr.phoneNumber || ''
};
if (prefix === 'billing' && walletAddr.emailAddress) {
result.billing_email = walletAddr.emailAddress;
}
return result;
}
isCheckoutPage() {
return this.ppcp_manager.page === 'checkout';
}
isProductPage() {
return this.ppcp_manager.page === 'product';
}
isCartPage() {
return this.ppcp_manager.page === 'cart';
}
isSale() {
return this.ppcp_manager.paymentaction === 'capture';
}
throttle(func, limit) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
func.apply(this, args);
}
};
}
debounce(func, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
debounce_google(func, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
debounce_apple(func, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
debounce_place_order(func, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
debounce_cc(func, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
bindCheckoutEvents() {
$('form.checkout').on('click', 'input[name="payment_method"]', () => {
this.debouncedTogglePlaceOrderButton();
$(document.body).trigger('wpg_change_method');
});
$('#order_review').on('click', 'input[name="payment_method"]', () => {
this.debouncedTogglePlaceOrderButton();
$(document.body).trigger('wpg_change_method');
});
if (this.ppcp_manager.is_pay_page === 'yes') {
this.debouncedTogglePlaceOrderButton();
}
$(document.body).on('ppcp_cc_checkout_updated ppcp_checkout_updated ppcp_cc_block_ready ppcp_block_ready', () => {
this.debouncedTogglePlaceOrderButton();
});
$('form.checkout').on('checkout_place_order_wpg_paypal_checkout_cc', (event) => {
event.preventDefault();
return this.handleCheckoutSubmit(event);
});
$('#order_review').on('submit', (event) => {
if (this.isPpcpCCSelected()) {
event.preventDefault();
return this.handleCheckoutSubmit();
}
});
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';
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';
$(document.body).on(eventSelectors, (event) => {
this.debouncedUpdatePaypalCheckout();
if (this.ppcp_manager.enabled_google_pay === 'yes') {
this.debouncedUpdateGooglePay();
}
if (this.ppcp_manager.enabled_apple_pay === 'yes') {
this.debouncedUpdateApplePay();
}
});
if (this.ppcp_manager.advanced_card_payments === 'yes') {
$(document.body).on(checkoutSelectors, () => {
this.debouncedUpdatePaypalCC();
});
}
}
handleCheckoutSubmit() {
var selected = $('input[name="wc-wpg_paypal_checkout_cc-payment-token"]:checked').val();
if (selected && selected !== 'new') {
return true; // only return true for saved token (not for 'new')
}
if (this.isPpcpCCSelected() && this.isCardFieldEligible()) {
if ($('form.checkout').hasClass('paypal_cc_submitting')) {
return false;
}
$('form.checkout').addClass('paypal_cc_submitting');
$(document.body).trigger('submit_paypal_cc_form');
return false;
}
return true;
}
syncPayPalCheckoutButtons() {
this.ppcp_cart_css();
this.renderSmartButton();
if ($('#ppcp_checkout_top').length === 0) {
const $applePay = $('div.apple-pay-container[data-context="express_checkout"]');
const $googlePay = $('div.google-pay-container[data-context="express_checkout"]');
const hasAppleOnly = $applePay.length > 0 && $googlePay.length === 0;
const hasGoogleOnly = $googlePay.length > 0 && $applePay.length === 0;
if (hasAppleOnly && !$applePay.hasClass('mobile')) {
$applePay.css('min-width', '480px');
}
if (hasGoogleOnly && !$googlePay.hasClass('mobile')) {
$googlePay.css('min-width', '480px');
}
}
}
syncPayPalCardFields() {
if (this.isCardFieldEligible()) {
this.renderCardFields();
$('#place_order, .wc-block-components-checkout-place-order-button').show();
} else {
$('.wc_payment_method.payment_method_wpg_paypal_checkout_cc').hide();
$('#radio-control-wc-payment-method-options-wpg_paypal_checkout_cc').parent('label').parent('div').hide();
if (this.isPpcpCCSelected())
$('#payment_method_wpg_paypal_checkout').prop('checked', true).trigger('click');
}
}
isPpcpSelected() {
if (this.ppcp_manager.is_wpg_change_payment_method === 'yes') {
return false;
}
return $('#payment_method_wpg_paypal_checkout').is(':checked') || $('input[name="radio-control-wc-payment-method-options"]:checked').val() === 'wpg_paypal_checkout';
}
isPpcpCCSelected() {
return $('#payment_method_wpg_paypal_checkout_cc').is(':checked') || $('input[name="radio-control-wc-payment-method-options"]:checked').val() === 'wpg_paypal_checkout_cc';
}
isCardFieldEligible() {
return this.isCheckoutPage() && this.ppcp_manager.advanced_card_payments === 'yes' && typeof wpg_paypal_sdk !== 'undefined' && wpg_paypal_sdk.CardFields().isEligible();
}
togglePlaceOrderButton() {
const isPpcpSelected = this.isPpcpSelected();
const isPpcpCCSelected = this.isPpcpCCSelected();
const usePlaceOrder = this.ppcp_manager.use_place_order === '1';
const PO_CLASSIC = '#place_order';
const PO_BLOCKS = '.wc-block-components-checkout-place-order-button';
const WALLETS = '#ppcp_checkout, #ppcp_order_pay, .google-pay-container.checkout, .apple-pay-container.checkout';
const setVisible = (sel, visible, displayShown = '') => {
const $el = jQuery(sel);
$el.toggleClass('wpg_place_order_hide', !visible);
$el.css('display', visible ? (displayShown || '') : 'none');
};
const showPO = () => {
setVisible(PO_CLASSIC, true, 'inline-block');
setVisible(PO_BLOCKS, true, 'inline-flex');
};
const hidePO = () => {
setVisible(PO_CLASSIC, false);
setVisible(PO_BLOCKS, false);
};
const showWallets = () => setVisible(WALLETS, true);
const hideWallets = () => setVisible(WALLETS, false);
if (isPpcpSelected) {
if (usePlaceOrder) {
hideWallets();
showPO();
} else {
hidePO();
showWallets();
}
} else {
showPO();
hideWallets();
}
if (isPpcpCCSelected && this.isCardFieldEligible()) {
showPO();
hideWallets();
}
}
onShippingChange(data, actions) {
if (!data || !data.shipping_address) {
return actions.reject();
}
// Simple address mapping (same as before)
const shippingAddress = {
city: data.shipping_address.city || '',
state: data.shipping_address.state || '',
countryCode: data.shipping_address.country_code || '',
postalCode: data.shipping_address.postal_code || ''
};
// NEW: detect which shipping method was selected in the PayPal UI
const selectedOption =
data.selected_shipping_option ||
data.selectedShippingOption ||
null;
const selectedShippingId =
selectedOption && selectedOption.id ? selectedOption.id : '';
// Validate address via AJAX (this will also update PayPal order)
return fetch(this.ppcp_manager.ajax_url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
action: 'ppcp_validate_shipping_address',
security: this.ppcp_manager.ajax_nonce,
shipping_address: JSON.stringify(shippingAddress),
// NEW: send selected shipping method id to PHP
selected_shipping_id: selectedShippingId
})
})
.then(res => res.json())
.then(result => {
if (result && result.success) {
return actions.resolve();
} else {
return actions.reject();
}
})
.catch(() => {
return actions.reject();
});
}
renderSmartButton() {
const selectors = this.ppcp_manager.button_selector;
$.each(selectors, (key, selector) => {
const elements = jQuery(".ppcp-button-container.ppcp_mini_cart");
if (elements.length > 1) {
elements.slice(0, -1).remove();
}
if (!$(selector).length || $(selector).children().length || typeof wpg_paypal_sdk === 'undefined') {
return;
}
const isExpressCheckout = selector === '#ppcp_checkout_top';
const isMiniCart = selector === '#ppcp_mini_cart' || selector === '#ppcp_mini_cart_block';
const ppcpStyle = {
layout: isMiniCart
? this.ppcp_manager.mini_cart_style_layout
: (isExpressCheckout ? this.ppcp_manager.express_checkout_style_layout : this.ppcp_manager.style_layout),
color: isMiniCart
? this.ppcp_manager.mini_cart_style_color
: (isExpressCheckout ? this.ppcp_manager.express_checkout_style_color : this.ppcp_manager.style_color),
shape: isMiniCart
? this.ppcp_manager.mini_cart_style_shape
: (isExpressCheckout ? this.ppcp_manager.express_checkout_style_shape : this.ppcp_manager.style_shape),
label: isMiniCart
? this.ppcp_manager.mini_cart_style_label
: (isExpressCheckout ? this.ppcp_manager.express_checkout_style_label : this.ppcp_manager.style_label),
height: Number(
isExpressCheckout
? this.ppcp_manager.express_checkout_button_height
: isMiniCart
? this.ppcp_manager.mini_cart_button_height
: this.ppcp_manager.button_height
) || 48
};
if (ppcpStyle.layout === 'horizontal') {
ppcpStyle.tagline = 'false';
}
const baseH = parseFloat(ppcpStyle.height) || 48; // handles "48" or "48px"
const heightPx = `${Math.max(0, baseH - 1)}px`; // reduce by 1px (no negatives)
const targets = [];
if (isMiniCart || selector === '#ppcp_cart') {
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');
} else if (isExpressCheckout) {
targets.push('#ppcp_checkout_top', '#ppcp_checkout_top_alternative',
'.google-pay-container.express_checkout', '.apple-pay-container.express_checkout');
} else if (selector === '#ppcp_product') {
targets.push('#ppcp_product', '.google-pay-container.product', '.apple-pay-container.product');
} else if (selector === '#ppcp_checkout') {
targets.push('#ppcp_checkout', '.google-pay-container.checkout', '.apple-pay-container.checkout');
}
if (targets.length) {
document.querySelectorAll(targets.join(',')).forEach(el => {
el.style.setProperty('--button-height', heightPx);
});
}
const styledFundingSources = [
wpg_paypal_sdk.FUNDING.PAYPAL,
wpg_paypal_sdk.FUNDING.PAYLATER
];
const addShippingCallbacks = selector !== '#ppcp_checkout';
if (selector === '#ppcp_checkout') {
let fundingSources = wpg_paypal_sdk.getFundingSources();
if (fundingSources.length) {
const hideTagline = this.ppcp_manager.is_google_pay_enabled_checkout === 'yes' || this.ppcp_manager.is_apple_pay_enable_checkout === 'yes';
fundingSources.forEach((fundingSource) => {
if (fundingSource === wpg_paypal_sdk.FUNDING.CARD && this.isCardFieldEligible()) {
return;
}
const options = {
fundingSource,
onClick: async (data, actions) => {
this.ppcp_used_payment_method = fundingSource;
if (this.ppcp_manager.is_block_enable === 'yes' && window.wp?.data) {
const vDisp = wp.data.dispatch('wc/store/validation');
const vSel = wp.data.select('wc/store/validation');
const cartSel = wp.data.select('wc/store/cart');
const chkSel = wp.data.select('wc/store/checkout');
// 0) Wait until Blocks is idle (addresses/rates/coupons/customer updates)
const waitUntilIdle = async () => {
for (let i = 0; i < 30; i++) {
const busy =
cartSel?.isLoading?.() ||
cartSel?.isCouponsUpdating?.() ||
cartSel?.isCustomerDataUpdating?.() ||
chkSel?.isProcessing?.(); // some builds expose this
if (!busy)
return;
await new Promise(r => setTimeout(r, 50));
}
};
await waitUntilIdle();
// 1) Ask Blocks to reveal native messages
vDisp.showAllValidationErrors();
// 2) Give the validation store a tick to recalc & render
await new Promise(r => setTimeout(r, 0));
// 3) Re-check using BOTH the boolean and the map (some builds lag the boolean)
const hasErrorsBool =
typeof vSel.hasValidationErrors === 'function' && vSel.hasValidationErrors();
const errorMap = typeof vSel.getValidationErrorMap === 'function'
? vSel.getValidationErrorMap()
: {};
const hasErrors = hasErrorsBool || (errorMap && Object.keys(errorMap).length > 0);
if (hasErrors) {
// keep focus on form; errors are already shown natively
return actions?.reject ? actions.reject() : false;
}
}
// proceed to wallet
return true;
}
,
createOrder: () => this.createOrder(selector),
onApprove: (data, actions) => this.onApproveHandler(data, actions),
onCancel: () => this.onCancelHandler(),
onError: (err) => this.onErrorHandler(err)
};
let style = {...ppcpStyle};
if (hideTagline) {
style.tagline = 'false';
const {layout, ...base} = style;
style = base; // remove layout
}
let cleanStyleBase = {...style};
if (styledFundingSources.includes(fundingSource)) {
options.style = {...cleanStyleBase};
} else {
const {color, ...cleanStyle} = cleanStyleBase;
options.style = {...cleanStyle};
}
const button = wpg_paypal_sdk.Buttons(options);
if (button.isEligible()) {
button.render(selector);
}
});
}
} else if (selector === '#ppcp_checkout_top') {
const expressFundingSources = [
wpg_paypal_sdk.FUNDING.PAYPAL,
wpg_paypal_sdk.FUNDING.VENMO,
wpg_paypal_sdk.FUNDING.PAYLATER,
wpg_paypal_sdk.FUNDING.CREDIT
];
const renderSelectors = ['#ppcp_checkout_top', '#ppcp_checkout_top_alternative'];
let renderedCount = 0;
for (let i = 0; i < expressFundingSources.length; i++) {
const fundingSource = expressFundingSources[i];
const targetSelector = renderSelectors[renderedCount];
if (!targetSelector || !$(targetSelector).length) {
continue;
}
const {layout, ...cleanStyle} = ppcpStyle;
let style = {...cleanStyle};
if (fundingSource === wpg_paypal_sdk.FUNDING.VENMO) {
style = {
...style,
color: 'blue'
};
} else if (fundingSource === wpg_paypal_sdk.FUNDING.CREDIT) {
style = {
...style,
color: 'darkblue'
};
}
const options = {
style,
fundingSource,
createOrder: () => this.createOrder(targetSelector),
onApprove: (data, actions) => this.onApproveHandler(data, actions),
onCancel: () => this.onCancelHandler(),
onError: (err) => this.onErrorHandler(err),
// Add this line only:
onShippingChange: addShippingCallbacks ? (data, actions) => this.onShippingChange(data, actions) : null
};
const button = wpg_paypal_sdk.Buttons(options);
if (button.isEligible()) {
button.render(targetSelector);
renderedCount++;
}
if (renderedCount >= 2) {
break;
}
}
if (renderedCount < 2 && $('#ppcp_checkout_top_alternative').length) {
if ($('div.apple-pay-container[data-context="express_checkout"]').length === 0 && $('div.google-pay-container[data-context="express_checkout"]').length === 0) {
if ($('#ppcp_checkout_top').length && !$('#ppcp_checkout_top').hasClass('mobile')) {
$('#ppcp_checkout_top').css('min-width', '480px');
}
}
$('#ppcp_checkout_top_alternative').remove();
}
} else {
const buttonOptions = {
style: ppcpStyle,
createOrder: () => this.createOrder(selector),
onApprove: (data, actions) => this.onApproveHandler(data, actions),
onCancel: () => this.onCancelHandler(),
onError: (err) => this.onErrorHandler(err)
};
if (addShippingCallbacks) {
buttonOptions.onShippingChange = (data, actions) => this.onShippingChange(data, actions);
}
wpg_paypal_sdk.Buttons(buttonOptions).render(selector);
}
});
var $targets = $('#ppcp_product, #ppcp_cart, #ppcp_mini_cart, #ppcp_mini_cart_block, #ppcp_checkout, #ppcp_checkout_top, #ppcp_checkout_top_alternative');
setTimeout(function () {
$targets.css({background: '', 'background-color': ''});
$targets.each(function () {
this.style.setProperty('--wpg-skel-fallback-bg', 'transparent');
});
$targets.addClass('bg-cleared');
}, 1);
}
getFromValue(selector) {
if (selector === '#ppcp_product') return 'product';
if (selector === '#ppcp_cart') return 'cart';
if (selector === '#ppcp_mini_cart' || selector === '#ppcp_mini_cart_block') return 'cart';
if (selector === '#ppcp_checkout_top') return 'express_checkout';
if (selector === '#ppcp_checkout') return 'checkout';
if (selector === '#ppcp_order_pay') return 'pay_page';
return 'checkout'; // fallback default
}
createOrder(selector) {
this.showSpinner();
$('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove();
let data;
const isMiniCart = selector === '#ppcp_mini_cart' || selector === '#ppcp_mini_cart_block';
if (isMiniCart) {
data = '';
} else if (selector === '#ppcp_checkout_top') {
} else if (this.isCheckoutPage()) {
data = $(selector).closest('form').serialize();
if (this.ppcp_manager.is_block_enable === 'yes') {
const notes = jQuery('.wc-block-components-textarea').val() || '';
data += '&customer_note=' + encodeURIComponent(notes);
const billingAddress = this.getBillingAddress();
const shippingAddress = this.getShippingAddress();
data += '&billing_address=' + encodeURIComponent(JSON.stringify(billingAddress));
data += '&shipping_address=' + encodeURIComponent(JSON.stringify(shippingAddress));
data += `&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`;
}
} else if (this.isProductPage()) {
$('', {type: 'hidden', name: 'ppcp-add-to-cart', value: $("[name='add-to-cart']").val()}).appendTo('form.cart');
data = $('form.cart').serialize();
} else {
data = $('form.woocommerce-cart-form').serialize();
}
const fundingMethod = this.ppcp_used_payment_method;
const from = this.getFromValue(selector);
let createOrderUrl = this.ppcp_manager.create_order_url_for_paypal
.replace(/([?&])from=[^&]*/i, '$1') // remove old `from`
.replace(/[?&]$/, ''); // cleanup
createOrderUrl += (createOrderUrl.includes('?') ? '&' : '?')
+ 'from=' + from
+ '&ppcp_used_payment_method=' + encodeURIComponent(fundingMethod);
return fetch(createOrderUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: data
}).then(res => res.json()).then(data => {
this.hideSpinner();
if (data.success !== undefined) {
const messages = data.data.messages ?? data.data;
this.showError(messages);
return null;
}
return data.orderID;
});
}
googleapplecreateOrder() {
this.showSpinner();
$('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove();
let data = '';
switch (this.pageContext) {
case 'checkout':
const selector = `[data-context="${this.pageContext}"]`;
data = $(selector).closest('form').serialize();
const isBlockCheckout = this.ppcp_manager.is_block_enable === 'yes';
if (isBlockCheckout && $('form.wc-block-checkout__form').length) {
const notes = jQuery('.wc-block-components-textarea').val() || '';
data += '&customer_note=' + encodeURIComponent(notes);
let billingAddress = this.getBillingAddress();
let shippingAddress = this.getShippingAddress();
// User-entered Block Checkout fields take precedence. Only fall back to
// the wallet address when the customer clicked an express wallet button
// without filling in the form, otherwise the WC order is created blank.
if ((!billingAddress || !billingAddress.billing_address_1) && this.walletBillingAddress) {
const walletBilling = this.walletToWCFormat(this.walletBillingAddress, 'billing');
if (walletBilling.billing_address_1) {
billingAddress = walletBilling;
}
}
if ((!shippingAddress || !shippingAddress.shipping_address_1) && this.walletShippingAddress) {
const walletShipping = this.walletToWCFormat(this.walletShippingAddress, 'shipping');
if (walletShipping.shipping_address_1) {
shippingAddress = walletShipping;
}
}
data += '&billing_address=' + encodeURIComponent(JSON.stringify(billingAddress));
data += '&shipping_address=' + encodeURIComponent(JSON.stringify(shippingAddress));
data += `&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`;
} else if ($('form.checkout').length) {
data = $('form.checkout').serialize();
} else if ($('form.woocommerce-cart-form').length) {
data = $('form.woocommerce-cart-form').serialize();
}
break;
case 'express_checkout':
case 'product':
break;
default:
data = $('form.woocommerce-cart-form').serialize();
break;
}
let createOrderUrl = this.ppcp_manager.create_order_url_for_google_pay;
createOrderUrl = createOrderUrl
.replace(/([?&])from=[^&]*/i, '$1')
.replace(/[?&]$/, '');
createOrderUrl += (createOrderUrl.includes('?') ? '&' : '?')
+ 'from=' + encodeURIComponent(this.pageContext);
return fetch(createOrderUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: data
}).then(res => res.json()).then(data => {
this.hideSpinner();
if (data.success !== undefined && data.success === false) {
const messages = typeof data.data === 'string'
? data.data
: (data.data.messages ?? 'An unknown error occurred.');
this.showError(messages);
throw new Error(messages);
return null;
}
return data.orderID;
});
}
onApproveHandler(data, actions) {
this.showSpinner();
const order_id = data.orderID || data.orderId || '';
const payer_id = data.payerID || data.payerId || '';
if (!order_id) {
return;
}
if (this.isCheckoutPage() || this.ppcp_manager.skip_order_review === 'yes') {
const url = `${this.ppcp_manager.cc_capture}&paypal_order_id=${encodeURIComponent(order_id)}&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`;
$.post(url, (response) => {
if (response?.data?.redirect) {
window.location.href = response.data.redirect;
} else {
if (response?.success === false) {
const messages = response.data?.messages ?? ['An unknown error occurred.'];
this.showError(messages);
this.hideSpinner();
let redirectUrl = `${this.ppcp_manager.checkout_url}?paypal_order_id=${encodeURIComponent(order_id)}&from=${this.ppcp_manager.page}`;
if (payer_id) {
redirectUrl += `&paypal_payer_id=${encodeURIComponent(payer_id)}`;
}
window.location.href = redirectUrl;
}
}
});
return;
}
let redirectUrl = `${this.ppcp_manager.checkout_url}?paypal_order_id=${encodeURIComponent(order_id)}&from=${this.ppcp_manager.page}`;
if (payer_id) {
redirectUrl += `&paypal_payer_id=${encodeURIComponent(payer_id)}`;
}
window.location.href = redirectUrl;
}
showSpinner(containerSelector = '.woocommerce') {
if (jQuery('.wc-block-checkout__main').length || jQuery('.wp-block-woocommerce-cart').length) {
jQuery('.wc-block-checkout__main, .wp-block-woocommerce-cart').block({message: null, overlayCSS: {background: '#fff', opacity: 0.6}});
} else if (jQuery('form.checkout').length) {
jQuery('form.checkout').block({message: null, overlayCSS: {background: '#fff', opacity: 0.6}});
} else if (jQuery(containerSelector).length) {
jQuery(containerSelector).block({message: null, overlayCSS: {background: '#fff', opacity: 0.6}});
}
if (jQuery('#wfacp-sec-wrapper').length) {
jQuery('#wfacp-sec-wrapper').block({message: null, overlayCSS: {background: '#fff', opacity: 0.6}});
}
}
hideSpinner(containerSelector = '.woocommerce') {
if (jQuery('.wc-block-checkout__main').length || jQuery('.wp-block-woocommerce-cart').length) {
jQuery('.wc-block-checkout__main, .wp-block-woocommerce-cart').unblock();
} else if (jQuery('form.checkout').length) {
jQuery('form.checkout').unblock();
} else if (jQuery(containerSelector).length) {
jQuery(containerSelector).unblock();
}
if (jQuery('#wfacp-sec-wrapper').length) {
jQuery('#wfacp-sec-wrapper').unblock();
}
}
onCancelHandler() {
window.location.reload(true);
this.hideSpinner();
}
onErrorHandler(err) {
this.hideSpinner();
}
showError(error_message) {
if (typeof error_message === 'undefined' || error_message === null) {
return;
}
let $checkout_form;
if ($('form.checkout').length) {
$checkout_form = $('form.checkout');
} else if ($('.woocommerce-notices-wrapper').length) {
$checkout_form = $('.woocommerce-notices-wrapper');
} else if ($('.woocommerce').length) {
$checkout_form = $('.woocommerce');
} else if ($('.wc-block-components-notices').length) {
$checkout_form = $('.wc-block-components-notices').first();
} else if ($('form#wfacp_checkout_form').length) {
$checkout_form = $('form#wfacp_checkout_form').first();
}
if ($checkout_form && $checkout_form.length) {
$('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove();
if (!error_message || (typeof error_message !== 'string' && !Array.isArray(error_message))) {
error_message = ['An unknown error occurred.'];
} else if (typeof error_message === 'string') {
error_message = [error_message];
} else if (error_message?.data?.messages && Array.isArray(error_message.data.messages)) {
error_message = error_message.data.messages;
}
let errorHTML = '
';
$.each(error_message, (index, value) => {
errorHTML += `- ${value}
`;
});
errorHTML += '
';
$checkout_form.prepend(errorHTML).removeClass('processing').unblock();
$checkout_form.find('.input-text, select, input:checkbox').trigger('validate').trigger('blur');
const scrollElement = $('.woocommerce-NoticeGroup-updateOrderReview, .woocommerce-NoticeGroup-checkout').filter(function () {
const $el = $(this);
if (!$el.length || !$el.is(':visible')) {
return false;
}
const offset = $el.offset?.();
return offset && typeof offset.top !== 'undefined';
}).first();
if (scrollElement.length) {
const offset = scrollElement.offset();
if (offset && typeof offset.top !== 'undefined') {
$('html, body').animate({scrollTop: offset.top - 100}, 1000);
}
}
//$(document.body).trigger('checkout_error', [error_message]);
} else {
const errorMessagesString = Array.isArray(error_message)
? error_message.join('
')
: typeof error_message === 'string'
? error_message
: 'An unknown error occurred.';
$(document.body).trigger('ppcp_checkout_error', errorMessagesString);
}
}
renderCardFields() {
jQuery(document.body).trigger('wc-credit-card-form-init');
const checkoutSelector = this.getCheckoutSelectorCss();
if ($('#wpg_paypal_checkout_cc-card-number').length === 0 || typeof wpg_paypal_sdk === 'undefined') {
return;
}
$(checkoutSelector).addClass('CardFields');
const cardStyle = {
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'},
'.invalid': {color: '#fa755a', border: '1px solid #fa755a', boxShadow: 'none'},
'::placeholder': {color: '#aab7c4'},
'input:focus': {outline: 'none', border: '1px solid #4a90e2', boxShadow: '0 0 4px rgba(74, 144, 226, 0.3)'},
'.valid': {border: '1px solid #3ac569', color: '#32325d', boxShadow: 'none'}
};
const cardFields = wpg_paypal_sdk.CardFields({
style: cardStyle,
createOrder: () => this.createCardOrder(checkoutSelector),
onApprove: (payload) => payload && payload.orderID ? this.submitCardFields(payload) : console.error("No valid payload returned during onApprove:", payload),
onError: (err) => {
this.handleCardFieldsError(err, checkoutSelector);
}
});
if (cardFields.isEligible()) {
if (($("#wpg_paypal_checkout_cc-card-number").html() || "").trim() === "") {
const numberField = cardFields.NumberField();
$("#wpg_paypal_checkout_cc-card-number").empty();
$("#wpg_paypal_checkout_cc-card-expiry").empty();
$("#wpg_paypal_checkout_cc-card-cvc").empty();
numberField.render("#wpg_paypal_checkout_cc-card-number");
numberField.setAttribute("placeholder", "1234 1234 1234 1234");
cardFields.ExpiryField().render("#wpg_paypal_checkout_cc-card-expiry");
cardFields.CVVField().render("#wpg_paypal_checkout_cc-card-cvc");
}
setTimeout(function () {
$('.wpg-paypal-cc-field label, .wpg-ppcp-card-cvv-icon').show();
}, 1600);
setTimeout(function () {
$('.wpg_ppcp_sanbdox_notice').show();
}, 1900);
} else {
console.log('Advanced Card Payments not Eligible', cardFields.isEligible());
$('.payment_box.payment_method_wpg_paypal_checkout_cc').hide();
if (this.isPpcpCCSelected()) {
$('#payment_method_wpg_paypal_checkout').prop('checked', true).trigger('click');
}
}
$(document.body).on('submit_paypal_cc_form', () => {
cardFields.submit().catch((error) => {
this.handleCardFieldsError(error, checkoutSelector);
});
});
}
createCardOrder(checkoutSelector) {
this.showSpinner();
$('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove();
let data;
if (this.ppcp_manager.is_block_enable === 'yes') {
data = $('form.wc-block-checkout__form').serialize();
const notes = jQuery('.wc-block-components-textarea').val() || '';
data += '&customer_note=' + encodeURIComponent(notes);
const billingAddress = this.getBillingAddress();
const shippingAddress = this.getShippingAddress();
data += '&billing_address=' + encodeURIComponent(JSON.stringify(billingAddress));
data += '&shipping_address=' + encodeURIComponent(JSON.stringify(shippingAddress));
data += `&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`;
} else {
data = $(checkoutSelector).closest('form').serialize();
}
return fetch(this.ppcp_manager.create_order_url_for_cc, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: data
})
.then(res => res.json())
.then(data => {
if (!data || data.success === false) {
const messages = data.data.messages ?? data.data;
this.hideSpinner();
this.showError(messages || 'An unknown error occurred while creating the order.');
return Promise.reject();
}
return data.orderID;
})
.catch(err => {
this.hideSpinner();
return Promise.reject();
});
}
submitCardFields(payload) {
$.post(`${this.ppcp_manager.cc_capture}&paypal_order_id=${payload.orderID}&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`, (data) => {
window.location.href = data.data.redirect;
});
}
handleCardFieldsError(errorString, checkoutSelector) {
$('#place_order, #wc-wpg_paypal_checkout-cc-form').unblock();
$(checkoutSelector).removeClass('processing paypal_cc_submitting CardFields createOrder').unblock();
const t = (key, fallback) => {
const v = (this.ppcp_manager && this.ppcp_manager[key]) ? String(this.ppcp_manager[key]) : '';
return v ? v : fallback;
};
const UNKNOWN_FALLBACK = "An unknown error occurred with your payment. Please try again.";
const INVALID_NUM_FALLBACK = "Please enter a valid card number.";
const INVALID_CVV_FALLBACK = "Please enter a valid CVV.";
const INVALID_EXP_FALLBACK = "Please enter a valid expiration date.";
let message = t('unknown_error', UNKNOWN_FALLBACK);
let raw = errorString instanceof Error ? errorString.message : String(errorString);
if (raw.includes('Expected reject')) {
return true;
}
try {
if (raw.includes('INVALID_NUMBER')) {
message = t('invalid_number', INVALID_NUM_FALLBACK);
} else if (raw.includes('INVALID_CVV')) {
message = t('invalid_cvv', INVALID_CVV_FALLBACK);
} else if (raw.includes('INVALID_EXPIRATION') || raw.includes('INVALID_EXPIRY_DATE')) {
message = t('invalid_expiry', INVALID_EXP_FALLBACK);
} else {
const jsonStart = raw.indexOf('{');
const jsonEnd = raw.lastIndexOf('}') + 1;
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
const jsonString = raw.slice(jsonStart, jsonEnd);
const error = JSON.parse(jsonString);
if (
(error.message && error.message.includes('Expected reject')) ||
(error.details?.[0]?.description && error.details[0].description.includes('Expected reject'))
) {
return true;
}
if (error.success === false && error.data && error.data.messages && Array.isArray(error.data.messages)) {
if (error.data.messages.some(msg => msg.includes('Expected reject'))) {
return true;
}
message = error.data.messages
.map(msg => msg.replace(/<\/?[^>]+(>|$)/g, ''))
.join('\n');
} else if (error.details && Array.isArray(error.details)) {
const cardError = error.details.find(detail =>
detail.issue === 'VALIDATION_ERROR' ||
detail.issue === 'INVALID_NUMBER' ||
detail.issue === 'INVALID_CVV' ||
detail.issue === 'INVALID_EXPIRY_DATE' ||
detail.field?.includes('/card/')
);
if (cardError) {
switch (cardError.issue) {
case 'INVALID_NUMBER':
message = t('invalid_number', INVALID_NUM_FALLBACK);
break;
case 'INVALID_CVV':
message = t('invalid_cvv', INVALID_CVV_FALLBACK);
break;
case 'INVALID_EXPIRY_DATE':
message = t('invalid_expiry', INVALID_EXP_FALLBACK);
break;
case 'VALIDATION_ERROR':
if (cardError.field?.includes('/card/number')) {
message = t('invalid_number', INVALID_NUM_FALLBACK);
} else if (cardError.field?.includes('/card/expiry')) {
message = t('invalid_expiry', INVALID_EXP_FALLBACK);
} else if (cardError.field?.includes('/card/security_code')) {
message = t('invalid_cvv', INVALID_CVV_FALLBACK);
}
break;
}
}
}
// If still default unknown message, try PayPal-provided message
if (message === t('unknown_error', UNKNOWN_FALLBACK)) {
if (error.details?.[0]?.description) {
message = error.details[0].description;
} else if (error.message) {
message = error.message;
}
}
}
}
} catch (err) {
if (raw.includes('INVALID_NUMBER')) {
message = t('invalid_number', INVALID_NUM_FALLBACK);
} else if (raw.includes('INVALID_CVV')) {
message = t('invalid_cvv', INVALID_CVV_FALLBACK);
} else if (raw.includes('INVALID_EXPIRATION') || raw.includes('INVALID_EXPIRY_DATE')) {
message = t('invalid_expiry', INVALID_EXP_FALLBACK);
} else {
const lastColon = raw.lastIndexOf(':');
message = lastColon > 0 ? raw.slice(lastColon + 1).trim() : raw;
}
}
message = String(message || '').replace(/\.$/, '').trim();
this.showError(message);
this.hideSpinner();
return false;
}
getCheckoutSelectorCss() {
return this.isCheckoutPage() ? 'form.checkout' : 'form.cart';
}
isCCPaymentMethodSelected() {
return this.getSelectedPaymentMethod() === 'wpg_paypal_checkout_cc';
}
getSelectedPaymentMethod() {
return $('input[name="payment_method"]:checked').val();
}
ppcp_cart_css() {
$('.payment_box.payment_method_wpg_paypal_checkout').each(function () {
if (($(this).html() || '').trim() === '') {
$(this).hide();
}
});
}
manageVariations(selector) {
if ($('.variations_form').length) {
const self = this;
const getCurrentProductId = () => {
const variationInput = document.querySelector('input.variation_id');
const variationId = parseInt(variationInput?.value || 0, 10);
return variationId > 0 ? variationId : parseInt(self.ppcp_manager?.product_id || 0, 10);
};
const getQty = () => parseFloat(document.querySelector('input.qty')?.value || '1') || 1;
$('.variations_form, .single_variation').on('show_variation', function (event, variation) {
if (variation.is_purchasable && variation.is_in_stock) {
$(selector).show();
self.fetchProductTotal(variation.variation_id || getCurrentProductId(), getQty());
} else {
$(selector).hide();
}
}).on('hide_variation', function () {
$(selector).hide();
});
$(document).off('change.wpg_product_qty input.wpg_product_qty', 'input.qty')
.on('change.wpg_product_qty input.wpg_product_qty', 'input.qty', () => {
self.fetchProductTotal(getCurrentProductId(), getQty());
});
}
}
manageSubscriptionOptions() {
const $radios = $('input[name^="convert_to_sub"]');
if (!$radios.length) {
return;
}
const toggleButtons = () => {
const val = $('input[name^="convert_to_sub"]:checked').val();
const isSubscription = val && val !== '0';
$('.google-pay-container.product, .apple-pay-container.product').toggle(!isSubscription);
$('#ppcp_product').find('[data-funding-source]').each(function () {
const source = $(this).attr('data-funding-source');
if (source === 'paypal' || source === 'card') {
$(this).show();
} else {
$(this).toggle(!isSubscription);
}
});
};
$radios.on('change', toggleButtons);
const observer = new MutationObserver(() => toggleButtons());
const productContainer = document.getElementById('ppcp_product');
if (productContainer) {
observer.observe(productContainer, { childList: true, subtree: true });
}
toggleButtons();
}
isPayPalGooglePaySdkReady() {
return typeof wpg_paypal_sdk !== "undefined" && typeof wpg_paypal_sdk.Googlepay !== "undefined" && typeof google !== "undefined";
}
removeGooglePayContainer() {
const containers = document.querySelectorAll('.google-pay-container');
containers.forEach(container => {
container.remove();
});
}
getGooglePaymentsClient( { requireOnPaymentDataChanged = false } = {}) {
if (typeof google === "undefined") {
return null;
}
if (!this.paymentsClient || this.paymentsClientRequiresDataChanged !== requireOnPaymentDataChanged) {
const paymentDataCallbacks = {
onPaymentAuthorized: this.onPaymentAuthorized.bind(this)
};
if (requireOnPaymentDataChanged) {
paymentDataCallbacks.onPaymentDataChanged = this.onPaymentDataChanged.bind(this);
}
this.paymentsClient = new google.payments.api.PaymentsClient({
environment: this.ppcp_manager.environment || "TEST",
paymentDataCallbacks
});
this.paymentsClientRequiresDataChanged = requireOnPaymentDataChanged;
}
return this.paymentsClient;
}
async onPaymentDataChanged(intermediatePaymentData) {
try {
const {callbackTrigger, shippingAddress, shippingOptionData} = intermediatePaymentData || {};
const mapAddr = (addr = {}) => ({
address1: addr.address1 || '',
address2: addr.address2 || '',
city: addr.locality || '',
state: addr.administrativeArea || '',
postcode: addr.postalCode || '',
country: addr.countryCode || ''
});
if (callbackTrigger === 'INITIALIZE') {
if (shippingAddress) {
const mapped = mapAddr(shippingAddress);
const vr = await this.validateShippingAddressFromBackend({
city: mapped.city,
state: mapped.state,
countryCode: mapped.country,
postalCode: mapped.postcode
});
if (!vr || vr.success !== true) {
const msg = (vr && vr.data) ? String(vr.data) : this.t('shipping_unserviceable', 'We do not ship to this address.');
return {
error: {
reason: 'SHIPPING_ADDRESS_UNSERVICEABLE',
message: msg,
intent: 'SHIPPING_ADDRESS'
}
};
}
await this.fetchUpdatedTotalFromBackend(mapped);
const tx = this.getGoogleTransactionInfo();
tx.totalPriceStatus = 'ESTIMATED';
return {
newTransactionInfo: tx,
newShippingOptionParameters: this.getGoogleShippingOptionParametersOrPlaceholder()
};
}
return {};
}
if (callbackTrigger === 'SHIPPING_ADDRESS' && shippingAddress) {
const selectedShippingId = shippingOptionData && shippingOptionData.id ? shippingOptionData.id : '';
const mapped = mapAddr(shippingAddress);
const vr = await this.validateShippingAddressFromBackend({
city: mapped.city,
state: mapped.state,
countryCode: mapped.country,
postalCode: mapped.postcode
}, selectedShippingId);
if (!vr || vr.success !== true) {
const msg = (vr && vr.data) ? String(vr.data) : this.t('shipping_unserviceable', 'We do not ship to this address.');
return {
error: {
reason: 'SHIPPING_ADDRESS_UNSERVICEABLE',
message: msg,
intent: 'SHIPPING_ADDRESS'
}
};
}
await this.fetchUpdatedTotalFromBackend(mapped, null, selectedShippingId);
const tx = this.getGoogleTransactionInfo();
tx.totalPriceStatus = 'ESTIMATED';
return {
newTransactionInfo: tx,
newShippingOptionParameters: this.getGoogleShippingOptionParametersOrPlaceholder()
};
}
if (callbackTrigger === 'SHIPPING_OPTION' && shippingOptionData && shippingOptionData.id) {
if (shippingOptionData.id === 'pending') {
const tx = this.getGoogleTransactionInfo();
tx.totalPriceStatus = 'ESTIMATED';
return {
newTransactionInfo: tx,
newShippingOptionParameters: this.getGoogleShippingOptionParametersOrPlaceholder()
};
}
const safeId = shippingOptionData.id;
const internalId = (this.googleShippingIdMap && this.googleShippingIdMap[safeId]) ? this.googleShippingIdMap[safeId] : safeId;
this.googleSelectedShippingId = internalId;
const addr = shippingAddress ? mapAddr(shippingAddress) : {};
await this.fetchUpdatedTotalFromBackend(addr, null, internalId);
const tx = this.getGoogleTransactionInfo();
tx.totalPriceStatus = 'ESTIMATED';
return {
newTransactionInfo: tx,
newShippingOptionParameters: this.getGoogleShippingOptionParametersOrPlaceholder()
};
}
return {};
} catch (e) {
console.error('onPaymentDataChanged error:', e);
return {};
}
}
async getGooglePayConfig() {
try {
if (!this.allowedPaymentMethods || !this.merchantInfo) {
const googlePayConfig = await wpg_paypal_sdk.Googlepay().config();
let methods = googlePayConfig.allowedPaymentMethods || [];
methods = methods.map(method => {
const m = { ...method };
m.parameters = { ...(m.parameters || {}) };
m.parameters.billingAddressRequired = true;
m.parameters.billingAddressParameters = {
...(m.parameters.billingAddressParameters || {}),
// FULL returns the street-level billing address (address1/locality/
// etc.). The default MIN format only returns name, country and
// postcode, which leaves the order billing address incomplete when
// the checkout form is not filled in.
format: 'FULL',
phoneNumberRequired: true,
};
return m;
});
this.allowedPaymentMethods = methods;
this.merchantInfo = googlePayConfig.merchantInfo || {};
}
return {
allowedPaymentMethods: this.allowedPaymentMethods,
merchantInfo: this.merchantInfo,
};
} catch (error) {
console.error("Failed to fetch Google Pay configuration:", error);
return { allowedPaymentMethods: [], merchantInfo: {} };
}
}
getGoogleIsReadyToPayRequest(allowedPaymentMethods) {
return {
apiVersion: 2,
apiVersionMinor: 0,
allowedPaymentMethods,
};
}
async syncGooglePayButton() {
if (this.ppcp_manager.enabled_google_pay !== 'yes') {
this.removeGooglePayContainer();
return;
}
if (!this.isPayPalGooglePaySdkReady()) {
console.log('[Google Pay] SDK not available (script not loaded or unsupported)');
this.removeGooglePayContainer();
return;
}
const googlePayConfig = await this.getGooglePayConfig();
const allowedPaymentMethods = googlePayConfig?.allowedPaymentMethods;
if (!allowedPaymentMethods || !allowedPaymentMethods.length) {
console.log('[Google Pay] No allowed payment methods');
this.removeGooglePayContainer();
return;
}
const shippingRequired = this.ppcp_manager.needs_shipping === "1";
const paymentsClient = this.getGooglePaymentsClient({requireOnPaymentDataChanged: shippingRequired});
try {
const response = await paymentsClient.isReadyToPay(this.getGoogleIsReadyToPayRequest(allowedPaymentMethods));
if (!response.result) {
console.log('[Google Pay] isReadyToPay returned false');
this.removeGooglePayContainer();
return;
}
document.querySelectorAll('.google-pay-container').forEach(container => {
container.innerHTML = '';
this.renderGooglePayButton(container);
});
} catch (error) {
console.error('[Google Pay] isReadyToPay failed', error);
this.removeGooglePayContainer();
}
}
renderGooglePayButton(container) {
if (!container) {
return;
}
const context = container.getAttribute('data-context') || 'product';
const labelMap = {
product: this.ppcp_manager.google_pay_style_label,
cart: this.ppcp_manager.google_pay_style_label,
checkout: this.ppcp_manager.google_pay_style_label,
express_checkout: this.ppcp_manager.google_pay_express_checkout_style_label,
mini_cart: this.ppcp_manager.google_pay_mini_cart_style_label
};
const colorMap = {
product: this.ppcp_manager.google_pay_style_color,
cart: this.ppcp_manager.google_pay_style_color,
checkout: this.ppcp_manager.google_pay_style_color,
express_checkout: this.ppcp_manager.google_pay_express_checkout_style_color,
mini_cart: this.ppcp_manager.google_pay_mini_cart_style_color
};
const shapeMap = {
product: this.ppcp_manager.google_pay_style_shape,
cart: this.ppcp_manager.google_pay_style_shape,
checkout: this.ppcp_manager.google_pay_style_shape,
express_checkout: this.ppcp_manager.google_pay_express_checkout_style_shape,
mini_cart: this.ppcp_manager.google_pay_mini_cart_style_shape
};
const heightMap = {
product: this.ppcp_manager.button_height,
cart: this.ppcp_manager.button_height,
checkout: this.ppcp_manager.button_height,
express_checkout: this.ppcp_manager.express_checkout_button_height,
mini_cart: this.ppcp_manager.mini_cart_button_height
};
const buttonType = labelMap[context] || 'plain';
const buttonColor = colorMap[context] || 'black';
const buttonShape = shapeMap[context] || 'rect';
const buttonHeight = parseInt(heightMap[context]) || 40;
let buttonRadius;
if (buttonShape === 'rect') {
buttonRadius = 4;
} else {
buttonRadius = Math.round(buttonHeight / 2);
}
const paymentsClient = this.getGooglePaymentsClient();
const button = paymentsClient.createButton({
buttonColor: buttonColor,
buttonType: buttonType,
buttonRadius: buttonRadius,
buttonLocale: this.ppcp_manager.locale,
buttonSizeMode: 'fill',
onClick: this.onGooglePaymentButtonClicked.bind(this)
});
button.setAttribute('data-context', context);
container.innerHTML = '';
container.appendChild(button);
button.querySelector('.gpay-button').style.setProperty('border-radius', `${buttonRadius}px`, 'important');
var $targets = $('.google-pay-container');
setTimeout(function () {
$targets.css({background: '', 'background-color': ''});
$targets.each(function () {
this.style.setProperty('--wpg-skel-fallback-bg', 'transparent');
});
$targets.addClass('bg-cleared');
}, 1);
}
async onGooglePaymentButtonClicked(event) {
try {
this.showSpinner();
const button = event?.target?.closest('button');
const clickedWrapper = button?.parentElement;
this.pageContext = clickedWrapper?.getAttribute('data-context') || 'unknown';
const transactionInfo = await this.ppcpGettransactionInfo();
if (transactionInfo?.success === false) {
const messages = transactionInfo.data?.messages ?? transactionInfo.data ?? ['Unknown error'];
this.showError(messages);
this.hideSpinner();
throw new Error(messages);
}
if (transactionInfo?.success && transactionInfo.data) {
if (typeof this.setTotalsFromResponse === 'function') {
this.setTotalsFromResponse(transactionInfo.data);
}
}
const shippingRequired = this.ppcp_manager.needs_shipping === "1" && this.pageContext !== 'checkout';
const paymentsClient = this.getGooglePaymentsClient({requireOnPaymentDataChanged: shippingRequired});
const paymentDataRequest = await this.getGooglePaymentDataRequest();
await paymentsClient.loadPaymentData(paymentDataRequest);
} catch (error) {
console.error('[GPay] loadPaymentData error:', error);
this.hideSpinner();
if (error?.statusCode === "CANCELED") {
window.location.reload(true);
console.warn("Google Pay was cancelled by the user.");
return;
}
console.error("Google Pay Button Click Error:", error);
}
}
getGoogleShippingOptionParameters() {
const methods = Array.isArray(this.ppcp_manager.shipping_methods) ? this.ppcp_manager.shipping_methods : [];
if (!methods.length) {
return null;
}
this.googleShippingIdMap = {};
const formatPrice = (amount) => {
const n = parseFloat(String(amount || "0").replace(/,/g, "")) || 0;
const currency = this.ppcp_manager.currency || "USD";
const locale = this.ppcp_manager.locale || "en-US";
return new Intl.NumberFormat(locale, {
style: "currency",
currency
}).format(n);
};
let defaultInternalId = this.googleSelectedShippingId;
if (!defaultInternalId) {
const selected = methods.find(m => m.is_selected);
defaultInternalId = selected?.id || methods[0].id;
}
const options = methods.map(method => {
const internalId = method.id;
const safeId = internalId.replace(/[^a-zA-Z0-9 _-]/g, "_");
this.googleShippingIdMap[safeId] = internalId;
const formattedAmount = formatPrice(method.amount);
return {
id: safeId,
label: method.label || internalId,
description: formattedAmount
};
});
let defaultSafeId = options[0].id;
const found = options.find(opt => this.googleShippingIdMap[opt.id] === defaultInternalId);
if (found) defaultSafeId = found.id;
return {
defaultSelectedOptionId: defaultSafeId,
shippingOptions: options
};
}
getGoogleShippingOptionParametersOrPlaceholder() {
const real = this.getGoogleShippingOptionParameters();
if (real && Array.isArray(real.shippingOptions) && real.shippingOptions.length > 0) {
return real;
}
return {
defaultSelectedOptionId: 'pending',
shippingOptions: [{
id: 'pending',
label: this.t('calculating_shipping', 'Shipping'),
description: this.t('calculating_shipping_desc', 'Calculated after address selection')
}]
};
}
async getGooglePaymentDataRequest() {
const {allowedPaymentMethods, merchantInfo} = await this.getGooglePayConfig();
const shippingRequired = this.ppcp_manager.needs_shipping === "1" && this.pageContext !== 'checkout';
const callbackIntents = ['PAYMENT_AUTHORIZATION'];
if (shippingRequired) {
callbackIntents.push('SHIPPING_ADDRESS', 'SHIPPING_OPTION');
}
const paymentDataRequest = {
apiVersion: 2,
apiVersionMinor: 0,
allowedPaymentMethods,
transactionInfo: this.getGoogleTransactionInfo(),
merchantInfo,
emailRequired: true,
callbackIntents
};
if (shippingRequired) {
paymentDataRequest.shippingAddressRequired = true;
paymentDataRequest.shippingAddressParameters = {
phoneNumberRequired: true
};
paymentDataRequest.shippingOptionRequired = true;
paymentDataRequest.shippingOptionParameters = this.getGoogleShippingOptionParametersOrPlaceholder();
}
return paymentDataRequest;
}
async ppcpGettransactionInfo() {
try {
this.showSpinner();
$('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success').remove();
let data = '';
const isBlockCheckout = this.ppcp_manager.is_block_enable === 'yes';
switch (this.pageContext) {
case 'checkout':
data = isBlockCheckout ? $('form.wc-block-checkout__form').serialize() : $('form.checkout').serialize();
if (isBlockCheckout) {
const billingAddress = this.getBillingAddress();
const shippingAddress = this.getShippingAddress();
data += '&billing_address=' + encodeURIComponent(JSON.stringify(billingAddress));
data += '&shipping_address=' + encodeURIComponent(JSON.stringify(shippingAddress));
data += `&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`;
}
break;
case 'product':
$('', {
type: 'hidden',
name: 'ppcp-add-to-cart',
value: $("[name='add-to-cart']").val()
}).appendTo('form.cart');
data = $('form.cart').serialize();
break;
case 'express_checkout':
break;
default:
data = $('form.woocommerce-cart-form').serialize();
break;
}
const transactionInfoUrl = `${this.ppcp_manager.get_transaction_info_url}&form=${encodeURIComponent(this.pageContext)}&used=google_pay`;
const response = await fetch(transactionInfoUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: data
});
return await response.json();
} catch (error) {
console.error('Error in ppcpGettransactionInfo:', error);
return null;
} finally {
this.hideSpinner();
}
}
getGoogleTransactionInfo() {
const currency = this.ppcp_manager?.currency || "USD";
const country = this.ppcp_manager?.country || "US";
const toNum = (v) => {
const f = parseFloat((v ?? "").toString().replace(/,/g, ""));
return Number.isFinite(f) ? f : 0;
};
const toStr = (v) => toNum(v).toFixed(2);
const displayItems = [];
const items = Array.isArray(this.ppcp_manager?.cart_items) ? this.ppcp_manager.cart_items : [];
for (const it of items) {
const name = it?.name ?? "Item";
const qty = Math.max(1, parseInt(it?.quantity, 10) || 1);
const lineTotal = ("subtotal" in it) ? toNum(it.subtotal) : toNum(it.price) * qty;
displayItems.push({
label: `${name}${qty > 1 ? ` × ${qty}` : ""}`,
type: "LINE_ITEM",
price: toStr(lineTotal)
});
}
const needsShipping = String(this.ppcp_manager?.needs_shipping ?? "0") === "1";
const shippingTotal = toNum(this.ppcp_manager?.shipping_total);
if (needsShipping && shippingTotal > 0) {
displayItems.push({
label: "Shipping",
type: "SHIPPING_OPTION",
price: toStr(shippingTotal)
});
}
const taxTotal = toNum(this.ppcp_manager?.tax_total);
if (taxTotal > 0) {
displayItems.push({
label: "Tax",
type: "TAX",
price: toStr(taxTotal)
});
}
const discountTotal = toNum(this.ppcp_manager?.discount_total);
if (discountTotal > 0) {
displayItems.push({
label: "Discount",
type: "DISCOUNT",
price: toStr(-discountTotal)
});
}
const total = toNum(this.ppcp_manager?.cart_total);
return {
displayItems,
currencyCode: currency,
countryCode: country,
totalPriceStatus: "ESTIMATED",
totalPrice: toStr(total),
totalPriceLabel: "Total",
checkoutOption: "DEFAULT"
};
}
async onPaymentAuthorized(paymentData) {
try {
// Always extract wallet addresses so googleapplecreateOrder() can use them
// as a fallback when the Block Checkout form has not been filled.
const billingRaw = paymentData?.paymentMethodData?.info?.billingAddress || {};
const shippingRaw = paymentData?.shippingAddress || {};
const email = paymentData?.email || '';
const billingAddress = {
name: billingRaw.name || '',
surname: '',
address1: billingRaw.address1 || '',
address2: billingRaw.address2 || '',
city: billingRaw.locality || '',
state: billingRaw.administrativeArea || '',
postcode: billingRaw.postalCode || '',
country: billingRaw.countryCode || '',
phoneNumber: billingRaw.phoneNumber || '',
emailAddress: email || ''
};
const shippingAddress = {
name: shippingRaw.name || '',
surname: '',
address1: shippingRaw.address1 || '',
address2: shippingRaw.address2 || '',
city: shippingRaw.locality || '',
state: shippingRaw.administrativeArea || '',
postcode: shippingRaw.postalCode || '',
country: shippingRaw.countryCode || '',
phoneNumber: shippingRaw.phoneNumber || ''
};
this.walletBillingAddress = billingAddress;
this.walletShippingAddress = shippingAddress;
if (this.pageContext !== 'checkout') {
await this.fetchUpdatedTotalFromBackend(shippingAddress, billingAddress);
}
if (this.googleSelectedShippingId === 'pending' || !this.googleSelectedShippingId) {
const methods = Array.isArray(this.ppcp_manager.shipping_methods) ? this.ppcp_manager.shipping_methods : [];
if (methods.length) {
const pick = methods.find(m => m.is_selected) || methods[0];
this.googleSelectedShippingId = pick.id;
}
}
const orderId = await this.googleapplecreateOrder();
if (!orderId) {
throw new Error("Order creation failed.");
}
const result = await wpg_paypal_sdk.Googlepay().confirmOrder({
orderId,
paymentMethodData: paymentData.paymentMethodData
}).catch(err => {
console.error('[Google Pay] confirmOrder error:', err);
throw err;
});
if (result && result.status === "PAYER_ACTION_REQUIRED") {
await wpg_paypal_sdk.Googlepay().initiatePayerAction({ orderId });
}
this.onApproveHandler({ orderID: orderId }, 'google_pay');
return { transactionState: "SUCCESS" };
} catch (error) {
this.showError(error.message || "Google Pay failed.");
this.hideSpinner();
return {
transactionState: "ERROR",
error: {
intent: "PAYMENT_AUTHORIZATION",
message: error.message || "Google Pay failed."
}
};
}
}
setTotalsFromResponse(d = {}) {
if (!d) {
return;
}
this.ppcp_manager.cart_total = d.total ?? d.cart_total ?? this.ppcp_manager.cart_total;
this.ppcp_manager.cart_items = Array.isArray(d.cart_items) ? d.cart_items : (this.ppcp_manager.cart_items || []);
this.ppcp_manager.shipping_total = d.shipping_total ?? this.ppcp_manager.shipping_total ?? "0.00";
this.ppcp_manager.tax_total = d.tax_total ?? this.ppcp_manager.tax_total ?? "0.00";
this.ppcp_manager.discount_total = d.discount_total ?? this.ppcp_manager.discount_total ?? "0.00";
this.ppcp_manager.currency = d.currency ?? this.ppcp_manager.currency;
this.ppcp_manager.needs_shipping = d.needs_shipping ?? this.ppcp_manager.needs_shipping ?? "0";
this.ppcp_manager.shipping_methods = Array.isArray(d.shipping_methods)
? d.shipping_methods
: (this.ppcp_manager.shipping_methods || []);
const selected = (this.ppcp_manager.shipping_methods || []).find(m => m.is_selected);
if (selected && selected.id) {
this.googleSelectedShippingId = selected.id;
this.appleSelectedShippingId = selected.id;
}
}
async validateShippingAddressFromBackend(shippingAddress, selectedShippingId = '') {
const params = new URLSearchParams({
action: 'ppcp_validate_shipping_address',
security: this.ppcp_manager.ajax_nonce,
shipping_address: JSON.stringify(shippingAddress || {})
});
if (selectedShippingId) {
params.append('selected_shipping_id', selectedShippingId);
}
const res = await fetch(this.ppcp_manager.ajax_url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params
});
// admin-ajax.php always returns 200, so parse body
const result = await res.json().catch(() => null);
return result;
}
async fetchUpdatedTotalFromBackend(shippingAddress, billingAddress = null, selectedShippingId = '') {
try {
const params = new URLSearchParams({
action: 'ppcp_get_updated_total',
security: this.ppcp_manager.ajax_nonce,
shipping_address: JSON.stringify(shippingAddress || {}),
billing_address: billingAddress ? JSON.stringify(billingAddress) : '',
context: this.pageContext
});
if (selectedShippingId) {
params.append('selected_shipping_id', selectedShippingId);
}
const res = await fetch(this.ppcp_manager.ajax_url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params
});
const result = await res.json();
if (result?.success && result.data) {
if (typeof this.setTotalsFromResponse === 'function') {
this.setTotalsFromResponse(result.data);
}
if (result.data.total != null) {
const n = parseFloat(String(result.data.total).replace(/,/g, ''));
return Number.isFinite(n) ? n.toFixed(2) : String(result.data.total);
}
}
const n = parseFloat(String(this.ppcp_manager.cart_total || '0').replace(/,/g, ''));
return Number.isFinite(n) ? n.toFixed(2) : (this.ppcp_manager.cart_total || '0.00');
} catch (e) {
console.error('Error fetching updated total:', e);
const n = parseFloat(String(this.ppcp_manager.cart_total || '0').replace(/,/g, ''));
return Number.isFinite(n) ? n.toFixed(2) : (this.ppcp_manager.cart_total || '0.00');
}
}
formatAmount(amount) {
if (typeof amount === 'number') {
return amount.toFixed(2);
}
if (typeof amount === 'string') {
const parsed = parseFloat(amount);
return isNaN(parsed) ? "0.00" : parsed.toFixed(2);
}
return "0.00";
}
prefetchProductTotal() {
const hasProductContainer = document.querySelector('#ppcp_product, .google-pay-container[data-context="product"], .apple-pay-container[data-context="product"]');
if (!hasProductContainer) {
const hasApplePayContainer = document.querySelector('.apple-pay-container');
if (hasApplePayContainer) {
this.fetchProductTotal(0, 0);
}
return;
}
const getCurrentProductId = () => {
const variationInput = document.querySelector('input.variation_id');
const variationId = parseInt(variationInput?.value || 0, 10);
return variationId > 0
? variationId
: parseInt(this.ppcp_manager?.product_id || 0, 10);
};
const getQty = () => parseFloat(document.querySelector('input.qty')?.value || '1') || 1;
this.fetchProductTotal(getCurrentProductId(), getQty());
}
fetchProductTotal(productId, quantity = 1) {
const data = new URLSearchParams({
action: 'ppcp_get_product_total',
security: this.ppcp_manager.ajax_nonce,
product_id: productId,
quantity: quantity
});
fetch(this.ppcp_manager.ajax_url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: data.toString()
}).then(res => res.json())
.then(response => {
if (response.success && response.data?.combined_total) {
this.ppcp_manager.cart_total = response.data.combined_total;
}
});
}
removeApplePayContainer() {
setTimeout(() => {
const containers = document.querySelectorAll('.apple-pay-container');
containers.forEach(container => {
container.remove();
});
}, 500);
}
async syncApplePayButton() {
if (this.ppcp_manager.enabled_apple_pay !== 'yes') {
this.removeApplePayContainer();
return;
}
if (window.location.protocol !== 'https:') {
console.log("Apple Pay requires HTTPS. Current protocol:", window.location.protocol);
this.removeApplePayContainer();
return;
}
if (!window.ApplePaySession) {
console.log("Apple Pay is not supported on this device.");
this.removeApplePayContainer();
return;
}
if (!ApplePaySession.canMakePayments()) {
console.log("Apple Pay cannot make payments on this device.");
this.removeApplePayContainer();
return;
}
if (typeof wpg_paypal_sdk === 'undefined' || typeof wpg_paypal_sdk.Applepay !== 'function') {
this.removeApplePayContainer();
return;
}
try {
const applepay = wpg_paypal_sdk.Applepay();
const config = await applepay.config({ environment: this.ppcp_manager.environment || "TEST" });
if (config && config.isEligible) {
this.renderApplePayButton();
} else {
console.log("Apple Pay is not eligible for this configuration.");
this.removeApplePayContainer();
}
} catch (error) {
console.error("Failed to load Apple Pay configuration:", error);
this.removeApplePayContainer();
}
}
renderApplePayButton() {
const containers = document.querySelectorAll(".apple-pay-container");
if (containers.length === 0) {
return;
}
this.prefetchProductTotal();
containers.forEach(container => {
container.innerHTML = '';
const applePayButton = document.createElement('apple-pay-button');
const context = container.getAttribute('data-context') || 'product';
const labelMap = {
product: this.ppcp_manager.apple_pay_style_label,
cart: this.ppcp_manager.apple_pay_style_label,
checkout: this.ppcp_manager.apple_pay_style_label,
express_checkout: this.ppcp_manager.apple_pay_express_checkout_style_label,
mini_cart: this.ppcp_manager.apple_pay_mini_cart_style_label
};
const colorMap = {
product: this.ppcp_manager.apple_pay_style_color,
cart: this.ppcp_manager.apple_pay_style_color,
checkout: this.ppcp_manager.apple_pay_style_color,
express_checkout: this.ppcp_manager.apple_pay_express_checkout_style_color,
mini_cart: this.ppcp_manager.apple_pay_mini_cart_style_color
};
const heightMap = {
product: this.ppcp_manager.button_height,
cart: this.ppcp_manager.button_height,
checkout: this.ppcp_manager.button_height,
express_checkout: this.ppcp_manager.express_checkout_button_height,
mini_cart: this.ppcp_manager.mini_cart_button_height
};
const shapeMap = {
product: this.ppcp_manager.apple_pay_style_shape,
cart: this.ppcp_manager.apple_pay_style_shape,
checkout: this.ppcp_manager.apple_pay_style_shape,
express_checkout: this.ppcp_manager.apple_pay_express_checkout_style_shape,
mini_cart: this.ppcp_manager.apple_pay_mini_cart_style_shape
};
const buttonType = labelMap[context] || 'plain';
const buttonColor = colorMap[context] || 'black';
const buttonHeight = parseInt(heightMap[context]) || 40;
const buttonShape = shapeMap[context] || 'rect';
const buttonRadius = buttonShape === 'pill' ? 20 : 4;
container.classList.add(buttonShape === 'pill' ? 'apple-shape-pill' : 'apple-shape-rect');
container.style.setProperty('--button-height', `${buttonHeight}px`);
container.style.setProperty('--button-radius', `${buttonRadius}px`);
container.style.height = `${buttonHeight}px`;
applePayButton.setAttribute('type', buttonType);
applePayButton.setAttribute('buttonstyle', buttonColor);
applePayButton.setAttribute('data-context', context);
container.appendChild(applePayButton);
applePayButton.addEventListener('click', () => this.onApplePayButtonClicked(container));
var $targets = $('.apple-pay-container');
setTimeout(function () {
$targets.css({background: '', 'background-color': ''});
$targets.each(function () {
this.style.setProperty('--wpg-skel-fallback-bg', 'transparent');
});
$targets.addClass('bg-cleared');
}, 1);
});
}
getAppleShippingMethods() {
const methods = Array.isArray(this.ppcp_manager.shipping_methods) ? this.ppcp_manager.shipping_methods : [];
if (!methods.length) {
return [];
}
this.appleShippingIdMap = {};
let defaultInternalId = this.appleSelectedShippingId;
if (!defaultInternalId) {
const selected = methods.find(m => m.is_selected);
defaultInternalId = (selected && selected.id) ? selected.id : methods[0].id;
}
this.appleSelectedShippingId = defaultInternalId;
const appleMethods = methods.map(method => {
const internalId = method.id;
const safeId = internalId.replace(/[^a-zA-Z0-9 _-]/g, "_");
this.appleShippingIdMap[safeId] = internalId;
return {
label: method.label || internalId,
amount: this.formatAmount(method.amount),
identifier: safeId,
detail: method.description || ''
};
});
return appleMethods;
}
onApplePayButtonClicked(container) {
try {
this.showSpinner();
this.pageContext = container?.getAttribute('data-context') || 'unknown';
if (!window.ApplePaySession || !ApplePaySession.canMakePayments()) {
console.warn('Apple Pay is not available on this device or browser.');
this.hideSpinner();
return;
}
const applepay = wpg_paypal_sdk.Applepay();
const needsShipping = this.ppcp_manager.needs_shipping === "1" && this.pageContext !== 'checkout';
const paymentRequest = {
countryCode: this.ppcp_manager.country || "US",
currencyCode: this.ppcp_manager.currency || "USD",
supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'],
merchantCapabilities: ['supports3DS'],
requiredBillingContactFields: ["postalAddress", "email", "phone", "name"],
total: {
label: this.ppcp_manager.store_label || "Total",
amount: this.formatAmount(this.ppcp_manager.cart_total),
type: "final"
}
};
if (needsShipping) {
paymentRequest.requiredShippingContactFields = ["postalAddress", "name", "phone", "email"];
const methods = this.getAppleShippingMethods();
if (methods.length) {
paymentRequest.shippingMethods = methods;
} else {
paymentRequest.shippingMethods = [{
label: 'Shipping',
amount: '0.00',
identifier: 'pending',
detail: 'Calculated after address selection'
}];
}
} else {
paymentRequest.requiredShippingContactFields = ["name", "phone", "email"];
}
const session = new ApplePaySession(4, paymentRequest);
session.onvalidatemerchant = async (event) => {
try {
const validation = await applepay.validateMerchant({
validationUrl: event.validationURL,
displayName: this.ppcp_manager.store_label || "Store"
});
session.completeMerchantValidation(validation.merchantSession);
} catch (err) {
console.error('Merchant validation failed:', err);
session.abort();
this.hideSpinner();
}
};
if (needsShipping) {
session.onshippingcontactselected = async (event) => {
try {
if (this.pageContext === 'product') {
const transactionInfo = await this.ppcpGettransactionInfo();
if (transactionInfo?.success === false) {
const messages = transactionInfo.data?.messages ?? transactionInfo.data ?? ['Unknown error'];
this.showError(messages);
throw new Error(messages);
session.completePayment({
status: ApplePaySession.STATUS_FAILURE
});
}
if (transactionInfo?.success && transactionInfo.data && typeof this.setTotalsFromResponse === 'function') {
this.setTotalsFromResponse(transactionInfo.data);
}
}
const shipping = event.shippingContact;
if (!shipping || !shipping.countryCode || !shipping.postalCode) {
throw new Error(this.t('shipping_address_incomplete', 'Shipping address is incomplete.'));
}
// ✅ Validate shipping country/address via the same AJAX endpoint used by PayPal onShippingChange
const vr = await this.validateShippingAddressFromBackend({
city: shipping.locality || '',
state: shipping.administrativeArea || '',
countryCode: shipping.countryCode || '',
postalCode: shipping.postalCode || ''
});
if (!vr || vr.success !== true) {
const msg = (vr && vr.data) ? String(vr.data) : this.t('shipping_unserviceable', 'We do not ship to this address.');
const errors = [];
if (window.ApplePayError) {
errors.push(new ApplePayError('shippingContactInvalid', 'postalAddress', msg));
}
session.completeShippingContactSelection({
errors,
newTotal: {
label: this.ppcp_manager.store_label || "Store",
amount: this.formatAmount(this.ppcp_manager.cart_total),
type: "final"
}
});
return;
}
const updatedTotal = await this.fetchUpdatedTotalFromBackend({
city: shipping.locality || '',
state: shipping.administrativeArea || '',
postcode: shipping.postalCode || '',
country: shipping.countryCode || ''
});
const methods = this.getAppleShippingMethods();
const update = {
newTotal: {
label: this.ppcp_manager.store_label || "Store",
amount: updatedTotal,
type: "final"
}
};
if (methods.length) {
update.newShippingMethods = methods;
}
session.completeShippingContactSelection(update);
} catch (error) {
console.error('[ApplePay] onshippingcontactselected error:', error);
const fallbackMethods = this.getAppleShippingMethods();
const update = {
newTotal: {
label: this.ppcp_manager.store_label || "Store",
amount: this.formatAmount(this.ppcp_manager.cart_total),
type: "final"
}
};
if (fallbackMethods.length) {
update.newShippingMethods = fallbackMethods;
}
session.completeShippingContactSelection(update);
}
};
session.onshippingmethodselected = async (event) => {
try {
const method = event.shippingMethod;
if (!method || !method.identifier) {
throw new Error(this.t('invalid_shipping_method', 'Invalid shipping method.'));
}
if (method.identifier === 'pending') {
session.completeShippingMethodSelection({
newTotal: {
label: this.ppcp_manager.store_label || "Store",
amount: this.formatAmount(this.ppcp_manager.cart_total),
type: "final"
}
});
return;
}
const internalId = this.appleShippingIdMap[method.identifier] || method.identifier;
this.appleSelectedShippingId = internalId;
const updatedTotal = await this.fetchUpdatedTotalFromBackend(
{}, // shipping address unchanged
null, // billing not needed here
internalId
);
const update = {
newTotal: {
label: this.ppcp_manager.store_label || "Store",
amount: updatedTotal,
type: "final"
}
};
session.completeShippingMethodSelection(update);
} catch (error) {
console.error('[ApplePay] onshippingmethodselected error:', error);
session.completeShippingMethodSelection({
newTotal: {
label: this.ppcp_manager.store_label || "Store",
amount: this.formatAmount(this.ppcp_manager.cart_total),
type: "final"
}
});
}
};
}
session.onpaymentauthorized = async (event) => {
try {
const transactionInfo = await this.ppcpGettransactionInfo();
if (transactionInfo.success === false) {
const messages = transactionInfo.data?.messages ?? transactionInfo.data ?? ['Unknown error'];
this.showError(messages);
throw new Error(messages);
session.completePayment({
status: ApplePaySession.STATUS_FAILURE
});
}
if (transactionInfo?.success) {
this.ppcp_manager.cart_total = transactionInfo.data?.cart_total || this.ppcp_manager.cart_total;
}
const billingRaw = event.payment?.billingContact || {};
const shippingRaw = event.payment?.shippingContact || {};
const emailAddress = billingRaw.emailAddress || shippingRaw.emailAddress || billingRaw.email || shippingRaw.email || '';
const billingAddress = {
name: billingRaw.givenName || '',
surname: billingRaw.familyName || '',
address1: billingRaw.addressLines?.[0] || '',
address2: billingRaw.addressLines?.[1] || '',
city: billingRaw.locality || '',
state: billingRaw.administrativeArea || '',
postcode: billingRaw.postalCode || '',
country: billingRaw.countryCode || '',
phoneNumber: billingRaw.phoneNumber || '',
emailAddress: emailAddress
};
const shippingAddress = {
name: shippingRaw.givenName || '',
surname: shippingRaw.familyName || '',
address1: shippingRaw.addressLines?.[0] || '',
address2: shippingRaw.addressLines?.[1] || '',
city: shippingRaw.locality || '',
state: shippingRaw.administrativeArea || '',
postcode: shippingRaw.postalCode || '',
country: shippingRaw.countryCode || '',
phoneNumber: shippingRaw.phoneNumber || ''
};
// Store wallet addresses so googleapplecreateOrder() can fall back to
// them when the Block Checkout form is empty.
this.walletBillingAddress = billingAddress;
this.walletShippingAddress = shippingAddress;
if (needsShipping) {
await this.fetchUpdatedTotalFromBackend(
shippingAddress,
billingAddress,
this.appleSelectedShippingId || ''
);
}
const orderId = await this.googleapplecreateOrder();
if (!orderId) {
throw new Error("Order creation failed.");
}
const result = await applepay.confirmOrder({
orderId: orderId,
token: event.payment.token,
billingContact: event.payment.billingContact
});
const status = result?.approveApplePayPayment?.status;
if (status === "APPROVED") {
this.showSpinner();
const order_id = orderId;
if (this.isCheckoutPage() || this.ppcp_manager.skip_order_review === 'yes') {
const url = `${this.ppcp_manager.cc_capture}&paypal_order_id=${encodeURIComponent(order_id)}&woocommerce-process-checkout-nonce=${this.ppcp_manager.woocommerce_process_checkout}`;
$.post(url, (response) => {
if (response?.data?.redirect) {
session.completePayment({
status: ApplePaySession.STATUS_SUCCESS
});
window.location.href = response.data.redirect;
} else {
if (response?.success === false) {
const messages = response.data?.messages ?? [this.t('unknown_error_short', 'An unknown error occurred.')];
this.showError(messages);
let redirectUrl = `${this.ppcp_manager.checkout_url}?paypal_order_id=${encodeURIComponent(order_id)}&from=${this.ppcp_manager.page}`;
window.location.href = redirectUrl;
this.hideSpinner();
}
}
});
} else {
session.completePayment({
status: ApplePaySession.STATUS_SUCCESS
});
let redirectUrl = `${this.ppcp_manager.checkout_url}?paypal_order_id=${encodeURIComponent(order_id)}&from=${this.ppcp_manager.page}`;
window.location.href = redirectUrl;
this.hideSpinner();
}
} else {
throw new Error(this.t('apple_pay_not_approved', 'Apple Pay confirmation returned non-APPROVED status.'));
}
} catch (error) {
session.completePayment({
status: ApplePaySession.STATUS_FAILURE
});
this.showError(error?.message || String(error) || 'An unknown error occurred.');
this.hideSpinner();
}
};
session.oncancel = () => {
window.location.reload(true);
console.log("Apple Pay session cancelled.");
this.hideSpinner();
};
session.begin();
} catch (err) {
console.error('Apple Pay session initialization failed:', err);
this.showError(this.t('apple_pay_init_failed', 'Apple Pay could not be initialized.'));
this.hideSpinner();
}
}
}
$(function () {
$('.woocommerce #payment #place_order, .woocommerce-page #payment #place_order').hide();
window.PPCPManager = PPCPManager;
const ppcp_manager = window.ppcp_manager || {};
window.ppcpManagerInstance = new PPCPManager(ppcp_manager);
});
})(jQuery);