1(function ($) { 2 'use strict'; 3 4 const settings = window.taylrGooglePayData || {}; 5 6 if (!settings.enable_googlepay || settings.enable_googlepay !== 'yes') { 7 return; 8 } 9 10 let paymentsClient = null; 11 const googlepay_environment = settings.googlepay_environment || 'TEST'; 12 13 // Shipping variables 14 let googleSelectedShippingId = ""; 15 let googleShippingOptions = []; 16 let googleShippingCost = {}; 17 let googleTax = 0; 18 let googleAmount = 0; 19 const googleCurrency = settings.currency_code || 'GBP'; 20 const googleNeedsShipping = settings.needs_shipping || false; 21 const googleAllowedCardNetworks = ['AMEX', 'DISCOVER', 'INTERAC', 'JCB', 'MASTERCARD', 'VISA']; 22 23 // Get product ID - handle both simple and variable products 24 let product_id = $('.single_add_to_cart_button').val() || $('input[name="add-to-cart"]').val(); 25 26 // Load Google Pay SDK 27 function loadGooglePaySDK(callback) { 28 if (window.google && window.google.payments && window.google.payments.api) { 29 callback(); 30 return; 31 } 32 const script = document.createElement('script'); 33 script.src = 'https://pay.google.com/gp/p/js/pay.js'; 34 script.async = true; 35 script.onload = callback; 36 script.onerror = function() {}; 37 document.head.appendChild(script); 38 } 39 40 // Get Google Pay PaymentsClient 41 // Note: Client is recreated if shipping callback is needed but wasn't included initially 42 function getPaymentsClient(needsShippingCallback = false) { 43 const shouldHaveShippingCallback = needsShippingCallback && googleShippingOptions.length > 0; 44 45 // Recreate client if we need shipping callback but don't have it 46 if (paymentsClient && shouldHaveShippingCallback && !paymentsClient._hasShippingCallback) { 47 paymentsClient = null; 48 } 49 50 if (!paymentsClient && window.google && window.google.payments && window.google.payments.api) { 51 paymentsClient = new google.payments.api.PaymentsClient({ 52 environment: googlepay_environment, 53 paymentDataCallbacks: { 54 onPaymentAuthorized: onPaymentAuthorized, 55 ...(shouldHaveShippingCallback && { onPaymentDataChanged: onPaymentDataChanged }) 56 }, 57 }); 58 paymentsClient._hasShippingCallback = shouldHaveShippingCallback; 59 } 60 return paymentsClient; 61 } 62 63 /** 64 * Fetch available shipping methods from WooCommerce 65 */ 66 async function getGoogleShippingOptions() { 67 try { 68 const response = await $.ajax({ 69 type: "POST", 70 url: settings.ajax_url, 71 data: { 72 action: 'taylr_get_shipping_methods', 73 page_name: 'product', 74 security: settings.nonce, 75 }, 76 }); 77 78 if (response.success && response.data.shipping_options_google) { 79 googleShippingOptions = response.data.shipping_options_google; 80 googleShippingCost = response.data.shipping_cost || {}; 81 googleSelectedShippingId = googleShippingOptions[0]?.id || ""; 82 } 83 } catch (error) { 84 // Shipping options fetch failed 85 } 86 } 87 88 /** 89 * Handles GooglePay Shipping Data Updates 90 */ 91 async function onPaymentDataChanged(paymentData) { 92 const callbackResponse = {}; 93 94 if (paymentData.shippingAddress || paymentData.shippingOptionData) { 95 try { 96 const response = await $.ajax({ 97 type: "POST", 98 url: settings.ajax_url, 99 data: { 100 action: 'taylr_validate_shipping_address', 101 address: paymentData.shippingAddress, 102 shipping_id: paymentData.shippingOptionData ? paymentData.shippingOptionData.id : "", 103 security: settings.nonce, 104 } 105 }); 106 107 if (!response.success) { 108 return { 109 error: { 110 reason: "SHIPPING_ADDRESS_UNSUPPORTED", 111 message: response.data?.message || 'Invalid shipping address', 112 intent: "SHIPPING_ADDRESS", 113 } 114 }; 115 } 116 117 // Update all shipping data from response 118 if (response.data.shipping_options_google && response.data.shipping_options_google.length > 0) { 119 googleShippingOptions = response.data.shipping_options_google; 120 // Only set new shipping options if address was changed 121 if (paymentData.shippingAddress) { 122 callbackResponse.newShippingOptionParameters = { 123 defaultSelectedOptionId: response.data.chosen_method || googleShippingOptions[0]?.id, 124 shippingOptions: googleShippingOptions 125 }; 126 } 127 } 128 129 if (response.data.shipping_costs) { 130 googleShippingCost = response.data.shipping_costs; 131 } 132 133 // Update amounts from backend 134 googleTax = response.data.tax_total || googleTax; 135 googleAmount = response.data.amount || googleAmount; 136 137 // If shipping option was selected, use backend calculated shipping total 138 if (paymentData.shippingOptionData) { 139 googleSelectedShippingId = paymentData.shippingOptionData.id; 140 } 141 142 const shippingAmount = response.data.shipping_total || googleShippingCost[googleSelectedShippingId] || 0; 143 144 // Update transaction info with backend calculated totals 145 callbackResponse.newTransactionInfo = { 146 totalPriceStatus: "FINAL", 147 totalPrice: ( 148 parseFloat(googleAmount) + 149 parseFloat(shippingAmount) + 150 parseFloat(googleTax) 151 ).toFixed(2), 152 currencyCode: googleCurrency, 153 }; 154 155 } catch (error) { 156 return { 157 error: {
158 reason: "SHIPPING_ADDRESS_ERROR", 159 message: "Error validating shipping address.", 160 intent: "SHIPPING_ADDRESS", 161 } 162 }; 163 } 164 } 165 166 return callbackResponse; 167 } 168 169 /** 170 * Add product to cart 171 */ 172 async function addToCart() { 173 let product_id = $('[name="add-to-cart"]').val(); 174 let variation_id = $('[name="variation_id"]').val(); 175 let quantity = $('[name="quantity"]').val() || 1; 176 const form = $('form.cart'); 177 const formDataArray = form.serializeArray(); 178 const data = { 179 action: 'taylr_add_to_cart', 180 product_id: product_id, 181 variation_id: variation_id, 182 quantity: quantity, 183 clear_cart: true, 184 security: settings.nonce, 185 }; 186 187 // Populate data object from form 188 formDataArray.forEach(function (item) { 189 data[item.name] = item.value; 190 }); 191 192 const response = await $.ajax({ 193 type: "POST", 194 url: settings.ajax_url, 195 data: data 196 }); 197 198 if (response.success) { 199 googleAmount = response.data.amount.toString(); 200 googleTax = response.data.tax_total || googleTax; 201 return true; 202 } else { 203 throw new Error(response.data?.message || 'Failed to add to cart'); 204 } 205 } 206 207 // Render Google Pay button 208 function renderGooglePayButton() { 209 const container = document.getElementById('taylr-google-pay-product-btn'); 210 if (!container) return; 211 212 const client = getPaymentsClient(); 213 if (!client) { 214 container.style.display = 'none'; 215 return; 216 } 217 218 container.style.display = 'block'; 219 container.innerHTML = ''; 220 221 const baseCardPaymentMethod = { 222 type: 'CARD', 223 parameters: { 224 allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], 225 allowedCardNetworks: googleAllowedCardNetworks 226 } 227 }; 228 229 const isReadyToPayRequest = { 230 apiVersion: 2, 231 apiVersionMinor: 0, 232 allowedPaymentMethods: [baseCardPaymentMethod] 233 }; 234 235 client.isReadyToPay(isReadyToPayRequest) 236 .then(function(response) { 237 if (response.result) { 238 const button = client.createButton({ 239 onClick: onGooglePaymentButtonClicked, 240 buttonType: 'buy', 241 buttonColor: 'black', 242 buttonSizeMode: 'fill' 243 }); 244 245 button.className = 'taylr-google-pay-button'; 246 container.appendChild(button); 247 248 const errorDiv = document.createElement('div'); 249 errorDiv.className = 'taylr-google-pay-product-error'; 250 errorDiv.style.display = 'none'; 251 errorDiv.style.color = 'red'; 252 errorDiv.style.marginTop = '10px'; 253 container.appendChild(errorDiv); 254 } else { 255 container.style.display = 'none'; 256 } 257 }) 258 .catch(function() { 259 container.style.display = 'none'; 260 }); 261 } 262 263 // Show error message 264 function showGooglePayError(msg) { 265 const errorDiv = document.querySelector('.taylr-google-pay-product-error'); 266 if (errorDiv) { 267 errorDiv.innerText = msg; 268 errorDiv.style.display = 'block'; 269 } 270 } 271 272 async function onGooglePaymentButtonClicked() { 273 if ($('.single_add_to_cart_button').hasClass('wc-variation-selection-needed')) { 274 alert("Please select some product options before adding this product to your cart."); 275 return; 276 } 277 278 let product_id = $('[name="add-to-cart"]').val(); 279 let variation_id = $('[name="variation_id"]').val(); 280 let quantity = $('[name="quantity"]').val() || 1; 281 const form = $('form.cart'); 282 const formDataArray = form.serializeArray(); 283 const data = { 284 action: 'taylr_add_to_cart', 285 product_id: product_id, 286 variation_id: variation_id, 287 quantity: quantity, 288 clear_cart: true, 289 security: settings.nonce, 290 }; 291 292 formDataArray.forEach(function (item) { 293 data[item.name] = item.value; 294 }); 295 296 if ($('.variations_form').length && !data['variation_id']) { 297 alert("Invalid variation selected."); 298 return; 299 } 300 301 try { 302 const response = await $.ajax({ 303 type: "POST", 304 url: settings.ajax_url, 305 data: data 306 }); 307 308 if (response.success) { 309 googleAmount = response.data.amount.toString(); 310 googleTax = response.data.tax_total || googleTax; 311 312 // Refresh shipping options after adding to cart 313 if (googleNeedsShipping) { 314 await getGoogleShippingOptions(); 315 } 316 317 const googlePayConfig = { 318 apiVersion: 2, 319 apiVersionMinor: 0, 320 allowedPaymentMethods: [{ 321 type: "CARD", 322 parameters: { 323 allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"], 324 allowedCardNetworks: googleAllowedCardNetworks, 325 billingAddressRequired: true, 326 billingAddressParameters: { format: "FULL", phoneNumberRequired: true }, 327 }, 328 tokenizationSpecification: { 329 type: "PAYMENT_GATEWAY", 330 parameters: { 331 gateway: 'crst', // STATIC as per GoogleSample reference 332 gatewayMerchantId: settings.merchant_id || '' // Taylr/Cardstream merchant ID 333 }, 334 } 335 }], 336 transactionInfo: { 337 totalPriceStatus: "FINAL", 338 totalPrice: ( 339 parseFloat(googleAmount) + 340 parseFloat(googleTax) 341 ).toFixed(2), 342 currencyCode: googleCurrency, 343 }, 344 merchantInfo: { 345 merchantName: settings.googlepay_merchant_name || settings.store_name || 'Store', 346 // Only include merchantId if it's set (required for PRODUCTION, optional for TEST) 347 ...(settings.googlepay_merchant_id && { merchantId: settings.googlepay_merchant_id }), 348 }, 349 emailRequired: true, 350 // Only include shipping intents if we have shipping options 351 callbackIntents: [ 352 "PAYMENT_AUTHORIZATION", 353 ...(googleNeedsShipping && googleShippingOptions.length > 0 ? ["SHIPPING_ADDRESS", "SHIPPING_OPTION"] : []) 354 ], 355 ...(googleNeedsShipping && googleShippingOptions.length > 0 && { 356 shippingAddressRequired: true, 357 shippingOptionRequired: true, 358 shippingOptionParameters: { 359 defaultSelectedOptionId: googleSelectedShippingId, 360 shippingOptions: googleShippingOptions, 361 }, 362 }), 363 }; 364 365 // Get client with appropriate shipping callback setting 366 const paymentsClient = getPaymentsClient(googleNeedsShipping); 367 try { 368 // loadPaymentData automatically triggers onPaymentAuthorized via paymentDataCallbacks 369 await paymentsClient.loadPaymentData(googlePayConfig); 370 } catch (error) { 371 if (error.statusCode !== 'CANCELED' && !error.message?.includes('User closed')) { 372 showGooglePayError('Google Pay payment failed. Please try again.'); 373 } 374 } 375 } else { 376 alert("Error: " + (response.data?.message || 'Failed to add to cart')); 377 } 378 } catch (error) { 379 alert("An error occurred: " + error.message); 380 } 381 } 382 383 function onPaymentAuthorized(paymentData) { 384 return new Promise(function (resolve, reject) { 385 handleGooglePaySuccess(paymentData) 386 .then(function () { 387 resolve({ transactionState: 'SUCCESS' }); 388 }) 389 .catch(function () { 390 resolve({
391 transactionState: 'ERROR', 392 error: { 393 intent: 'PAYMENT_AUTHORIZATION', 394 message: 'Something went wrong', 395 reason: 'PAYMENT_DATA_INVALID' 396 } 397 }); 398 }); 399 }); 400 } 401 402 /** 403 * Handles successful GooglePay transaction - Lloyds-style clean request/response 404 */ 405 function handleGooglePaySuccess(paymentData) { 406 return new Promise(function (resolve, reject) { 407 setTimeout(async function () { 408 try { 409 $.blockUI({ message: null, baseZ: 0 }); 410 411 // Send payment data directly as JSON (Lloyds approach - no base64 encoding) 412 const response = await $.ajax({ 413 type: 'POST', 414 url: settings.ajax_url, 415 data: { 416 action: 'taylr_process_google_pay_order', 417 paymentData: JSON.stringify(paymentData), 418 addresses: JSON.stringify({ 419 shippingAddress: paymentData.shippingAddress, 420 billingAddress: paymentData.paymentMethodData?.info?.billingAddress, 421 }), 422 email: paymentData.email, 423 selectedShippingId: googleSelectedShippingId, 424 security: settings.nonce, 425 }, 426 }); 427 428 $.unblockUI(); 429 430 if (response.success) { 431 resolve(); 432 setTimeout(function() { 433 window.location.href = response.data.redirect; 434 }, 500); 435 } else { 436 reject(new Error(response.data?.message || 'Transaction failed')); 437 } 438 } catch (error) { 439 $.unblockUI(); 440 reject(error); 441 } 442 }, 3000); 443 }); 444 } 445 446 // Insert Google Pay button on product page 447 function insertProductGooglePayButton() { 448 const addToCartButton = document.querySelector('.single_add_to_cart_button') || 449 document.querySelector('button[name="add-to-cart"]') || 450 document.querySelector('.wp-block-woocommerce-product-button'); 451 452 if (addToCartButton && !document.getElementById('taylr-google-pay-product-btn')) { 453 const container = document.createElement('div'); 454 container.id = 'taylr-google-pay-product-btn'; 455 container.style.marginTop = '15px'; 456 addToCartButton.parentNode.insertBefore(container, addToCartButton.nextSibling); 457 } 458 } 459 460 // Initialize Google Pay 461 async function initGooglePay() { 462 try { 463 insertProductGooglePayButton(); 464 465 // Pre-load shipping options if needed 466 if (googleNeedsShipping) { 467 await getGoogleShippingOptions(); 468 } 469 470 // Wait for Google Pay API 471 function waitForGooglePayAPI() { 472 if (window.google && window.google.payments && window.google.payments.api) { 473 renderGooglePayButton(); 474 } else { 475 setTimeout(waitForGooglePayAPI, 100); 476 } 477 } 478 waitForGooglePayAPI(); 479 480 // Re-initialize when variation changes (for variable products) 481 $(document.body).on('found_variation', function(event, variation) { 482 // Variation changed, re-fetch shipping options if needed 483 if (googleNeedsShipping) { 484 getGoogleShippingOptions().catch(function() {}); 485 } 486 }); 487 } catch (error) { 488 // Initialization failed silently 489 } 490 } 491 492 // Initialize on page load 493 $(document).ready(function() { 494 loadGooglePaySDK(function() { 495 function waitForGooglePayAPI() { 496 if (window.google && window.google.payments && window.google.payments.api) { 497 initGooglePay(); 498 } else { 499 setTimeout(waitForGooglePayAPI, 100); 500 } 501 } 502 waitForGooglePayAPI(); 503 }); 504 }); 505 506})(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.