1// utils.js - Optimized with better error handling and caching 2 3let paymentRequestRef = null; // This replaces the useRef in React 4let paymentRequest = null; // This replaces the useState in React 5let lastPaymentRequestHash = null; // Cache key for payment request 6let ajaxRequestCache = {}; // Cache for AJAX responses 7const CACHE_DURATION = 5000; // 5 seconds cache duration 8 9/** 10 * Strip shipping options down to the {id, label, amount} shape the Square Web 11 * Payments SDK accepts. Wallet sheets validate these shapes strictly: an extra 12 * key makes the whole list invalid, which left Apple Pay with no shipping 13 * selector at all. The server no longer sends extras, so this is belt and 14 * braces for cached/older responses. 15 * 16 * @param {Object} data Payment request or event response data. 17 * @return {Object} Same data with sanitized shippingOptions. 18 */ 19function sanitizeShippingOptionsForSheet( data ) { 20 if ( ! data || ! Array.isArray( data.shippingOptions ) ) { 21 return data; 22 } 23 return { 24 ...data, 25 shippingOptions: data.shippingOptions.map( ( option ) => ( { 26 id: option.id, 27 label: option.label, 28 amount: option.amount, 29 } ) ), 30 }; 31} 32 33async function initPaymentRequest( payments, needsShipping, couponApplied ) { 34 if ( ! payments ) { 35 console.error( 36 'Payments object is required to create payment request.' 37 ); 38 return null; 39 } 40 41 // Get the selected payment method from WooCommerce 42 const selectedPaymentMethod = jQuery( 43 'input[name="payment_method"]:checked' 44 ).val(); 45 const selectedShippingMethod = jQuery( 46 'input[name="shipping_method[0]"]:checked' 47 ).val(); 48 49 // Ensure we are only processing if the selected method is Google Pay or Apple Pay 50 if ( selectedPaymentMethod !== 'squaresync_credit' ) { 51 return null; 52 } 53 54 // Create cache key based on current state 55 const cacheKey = `${ needsShipping }-${ couponApplied }-${ selectedPaymentMethod }-${ selectedShippingMethod }`; 56 57 // Check if we can reuse the existing payment request 58 if ( lastPaymentRequestHash === cacheKey && paymentRequestRef ) { 59 return paymentRequestRef; 60 } 61 62 // Fetch and create or update the payment request 63 try { 64 const __paymentRequestJson = await getPaymentRequest(); 65 try { 66 // The server already returns the correct structure: 67 // lineItems (products, pre-tax) + taxLineItems + shippingOptions, 68 // summing to total. Only the shipping option shape is normalised. 69 const __paymentRequestObject = sanitizeShippingOptionsForSheet( 70 JSON.parse( __paymentRequestJson ) 71 ); 72 if ( __paymentRequestObject ) { 73 // Always request billing contact for Apple Pay / Google Pay 74 __paymentRequestObject.requestBillingContact = true; 75 76 // Check if wallet shipping collection is enabled (default: yes) 77 const walletCollectShipping = 78 SquareConfig.walletCollectShipping !== 'no'; 79 80 // Always set shipping flags if shippingOptions exist (from PHP) or needsShipping is true 81 // but only if wallet shipping collection is enabled 82 if ( 83 walletCollectShipping && 84 ( needsShipping || 85 ( __paymentRequestObject.shippingOptions && 86 __paymentRequestObject.shippingOptions.length > 87 0 ) ) 88 ) { 89 __paymentRequestObject.requestShippingAddress = true; 90 __paymentRequestObject.requestShippingContact = true; // Required for Apple Pay 91 } else if ( ! walletCollectShipping ) { 92 // When shipping collection is disabled, don't request from wallet 93 __paymentRequestObject.requestShippingAddress = false; 94 __paymentRequestObject.requestShippingContact = false; 95 } 96 } 97 98 if ( ! paymentRequestRef ) { 99 // Create a new payment request 100 paymentRequestRef = payments.paymentRequest( 101 __paymentRequestObject 102 ); 103 paymentRequest = paymentRequestRef; // Set state 104 } else { 105 // Update the existing payment request 106 paymentRequestRef.update( __paymentRequestObject ); 107 } 108 109 lastPaymentRequestHash = cacheKey; // Update cache key 110 return paymentRequestRef; // Return the created/updated payment request 111 } catch ( error ) { 112 console.error( 113 'Failed to create or update payment request:', 114 error 115 ); 116 return null; 117 } 118 } catch ( error ) { 119 console.error( 120 'Error fetching payment request from the server:', 121 error 122 ); 123 return null; 124 } 125} 126 127function getPaymentRequest() { 128 return new Promise( ( resolve, reject ) => { 129 const data = { 130 context: SquareConfig.context, 131 security: SquareConfig.paymentRequestNonce, 132 is_pay_for_order_page: false, 133 }; 134 135 // Check if we have a recent cached response 136 const cacheKey = 'payment_request'; 137 const cachedData = ajaxRequestCache[ cacheKey ]; 138 if ( 139 cachedData && 140 Date.now() - cachedData.timestamp < CACHE_DURATION 141 ) { 142 resolve( cachedData.data ); 143 return; 144 } 145 146 jQuery.ajax( { 147 url: SquareConfig.ajax_url, 148 type: 'POST', 149 timeout: 10000, // 10 second timeout 150 data: { 151 action: 'get_payment_request', // Your PHP handler action 152 ...data, // Spread the data into the request 153 }, 154 success: function ( response ) { 155 if ( response.success ) { 156 // Cache the successful response 157 ajaxRequestCache[ cacheKey ] = { 158 data: response.data, 159 timestamp: Date.now(), 160 }; 161 resolve( response.data ); // Resolve with the data 162 } else { 163 reject( response.data || 'Unknown error' ); // Reject with the error data 164 } 165 }, 166 error: function ( xhr, status, error ) { 167 const errorMsg = 168 status === 'timeout' 169 ? 'Request timed out. Please check your connection.' 170 : 'Error occurred: ' + 171 ( error || xhr.statusText || 'Unknown error' ); 172 reject( errorMsg ); 173 }, 174 } ); 175 } ); 176} 177 178async function getAjaxUrl( action ) { 179 return SquareConfig.wc_ajax_url.replace( 180 '%%endpoint%%', 181 `square_digital_wallet_${ action }` 182 ); 183} 184 185async function recalculateTotals( data ) { 186 return new Promise( ( resolve, reject ) => { 187 // Use WC AJAX endpoint (same as blocks checkout) for proper cart session handling
188 const wcAjaxUrl = SquareConfig.wc_ajax_url.replace( 189 '%%endpoint%%', 190 'square_digital_wallet_recalculate_totals' 191 ); 192 jQuery.ajax( { 193 url: wcAjaxUrl, 194 type: 'POST', 195 data: data, 196 success: function ( response ) { 197 if ( response.success ) { 198 resolve( response.data ); // Resolve with the data 199 } else { 200 reject( response.data ); // Reject with the error data 201 } 202 }, 203 error: function ( xhr, status, error ) { 204 reject( 'Error occurred: ' + ( error || xhr.statusText ) ); 205 }, 206 } ); 207 } ); 208} 209 210async function handleShippingAddressChanged( shippingContact ) { 211 const data = { 212 context: 'checkout', 213 shipping_contact: shippingContact, 214 security: SquareConfig.recalculate_totals_nonce, 215 is_pay_for_order_page: 'false', 216 }; 217 218 try { 219 const response = await recalculateTotals( data ); 220 return sanitizeShippingOptionsForSheet( response ); 221 } catch ( error ) { 222 console.error( 'Error recalculating totals:', error ); 223 // An error keeps the sheet open with a message. An empty 224 // shippingOptions array would instead tell the wallet the address is 225 // unserviceable and remove the shipping selector entirely. 226 return { 227 error: 'Unable to calculate shipping for this address. Please try again.', 228 }; 229 } 230} 231 232/** 233 * Handle shipping option changed event for Apple Pay / Google Pay. 234 * Recalculates totals and returns updated data to the wallet. 235 * 236 * @param {Object} option 237 * Example: { amount: "15.00", id: "flat_rate:1", label: "Flat rate" } 238 * @return {Object} Updated lineItems, shippingOptions, and total 239 */ 240async function handleShippingOptionChanged( option ) { 241 // Store the selected shipping option for later use 242 if ( option && option.id ) { 243 window._selectedShippingOption = option; 244 } 245 246 const data = { 247 context: 'checkout', 248 shipping_option: option.id, 249 security: SquareConfig.recalculate_totals_nonce, 250 is_pay_for_order_page: 'false', 251 }; 252 253 try { 254 const response = await recalculateTotals( data ); 255 // Return updated totals to Apple Pay / Google Pay 256 return sanitizeShippingOptionsForSheet( response ); 257 } catch ( error ) { 258 console.error( 'Error recalculating totals:', error ); 259 // Surface the failure instead of resolving with nothing, which left 260 // the sheet showing a total the server never agreed to. 261 return { 262 error: 'Unable to update shipping. Please try again.', 263 }; 264 } 265} 266 267/** 268 * Handle the afterpay_shippingaddresschanged event. 269 * 270 * Afterpay's modal requires shipping options in the extended 271 * AfterpayShippingOptionUpdate shape (per-option taxLineItems + nested total) 272 * that the strict Apple/Google sheets reject â the sws_afterpay flag makes 273 * the server return that shape, and the event return carries ONLY 274 * shippingOptions. 275 * 276 * @param {Object} shippingContact Address chosen in the Afterpay modal. 277 * @return {Object} Afterpay shipping option update. 278 */ 279async function handleAfterpayShippingAddressChanged( shippingContact ) { 280 const data = { 281 context: 'checkout', 282 shipping_contact: shippingContact, 283 security: SquareConfig.recalculate_totals_nonce, 284 is_pay_for_order_page: 'false', 285 sws_afterpay: '1', 286 }; 287 288 try { 289 const response = await recalculateTotals( data ); 290 return { shippingOptions: response.shippingOptions }; 291 } catch ( error ) { 292 console.error( 'Error recalculating totals:', error ); 293 return { 294 error: 'Unable to calculate shipping for this address. Please try again.', 295 }; 296 } 297} 298 299/** 300 * Handle the afterpay_shippingoptionchanged event. 301 * 302 * @param {Object} option Selected shipping option ({ id, label, amount }). 303 * @return {Object} Afterpay shipping option update. 304 */ 305async function handleAfterpayShippingOptionChanged( option ) { 306 // Store the choice so the checkout form's shipping radio can be synced 307 // after the Afterpay modal closes (wallets.js reads both keys). 308 if ( option && option.id ) { 309 window._selectedShippingOption = option; 310 window._afterpaySelectedShipping = option; 311 } 312 313 const data = { 314 context: 'checkout', 315 shipping_option: option.id, 316 security: SquareConfig.recalculate_totals_nonce, 317 is_pay_for_order_page: 'false', 318 sws_afterpay: '1', 319 }; 320 321 try { 322 const response = await recalculateTotals( data ); 323 return { shippingOptions: response.shippingOptions }; 324 } catch ( error ) { 325 console.error( 'Error recalculating totals:', error ); 326 return { 327 error: 'Unable to update shipping. Please try again.', 328 }; 329 } 330} 331 332function attachTokenToForm( token ) { 333 // CRITICAL: Remove any existing payment tokens BEFORE adding the new one
334 // This prevents duplicate hidden inputs where the browser might submit the old token 335 jQuery( 'input[name="square_payment_token"]' ).remove(); 336 337 // Handle both checkout and pay-for-order forms 338 const $form = jQuery( 'form.checkout, form#order_review' ); 339 jQuery( '<input>' ) 340 .attr( { 341 type: 'hidden', 342 name: 'square_payment_token', 343 value: token, 344 } ) 345 .prependTo( $form ); 346} 347 348function attachTokenToMyAccountForm( token ) { 349 // Remove any existing payment token first 350 jQuery( '#add_payment_method input[name="square_payment_token"]' ).remove(); 351 352 jQuery( '<input>' ) 353 .attr( { 354 type: 'hidden', 355 name: 'square_payment_token', 356 value: token, 357 } ) 358 .prependTo( '#add_payment_method' ); 359} 360 361function attachVerificationTokenToForm( verificationToken ) { 362 // CRITICAL: Remove any existing verification tokens BEFORE adding the new one 363 jQuery( 'input[name="square_verification_token"]' ).remove(); 364 365 // Handle both checkout and pay-for-order forms 366 const $form = jQuery( 'form.checkout, form#order_review' ); 367 jQuery( '<input>' ) 368 .attr( { 369 type: 'hidden', 370 name: 'square_verification_token', 371 value: verificationToken, 372 } ) 373 .prependTo( $form ); 374} 375 376// Function to check if shipping is required 377function checkNeedsShipping() { 378 return new Promise( ( resolve, reject ) => { 379 // Check cache first 380 const cacheKey = 'needs_shipping'; 381 const cachedData = ajaxRequestCache[ cacheKey ]; 382 if ( 383 cachedData && 384 Date.now() - cachedData.timestamp < CACHE_DURATION * 2 385 ) { 386 resolve( cachedData.data ); 387 return; 388 } 389 390 jQuery.ajax( { 391 url: SquareConfig.ajax_url, // WooCommerce AJAX URL 392 type: 'POST', 393 timeout: 5000, // 5 second timeout 394 data: { 395 action: 'get_needs_shipping', // Action defined in your PHP class 396 }, 397 success: function ( response ) { 398 if ( response.success ) { 399 // Cache the response 400 ajaxRequestCache[ cacheKey ] = { 401 data: response.data.needs_shipping, 402 timestamp: Date.now(), 403 }; 404 resolve( response.data.needs_shipping ); 405 } else { 406 reject( 'Failed to retrieve shipping information.' ); 407 } 408 }, 409 error: function ( xhr, status, error ) { 410 const errorMsg = 411 status === 'timeout' 412 ? 'Request timed out' 413 : 'Error occurred: ' + ( error || 'Unknown error' ); 414 reject( errorMsg ); 415 }, 416 } ); 417 } ); 418} 419 420// Utility function to log payment errors to a specific container 421function logPaymentError( message ) { 422 const paymentStatusContainer = document.getElementById( 423 'payment-status-container' 424 ); 425 if ( paymentStatusContainer ) { 426 // Clear any previous errors 427 paymentStatusContainer.innerHTML = ''; 428 429 // Create error message element 430 const errorMessage = document.createElement( 'p' ); 431 errorMessage.classList.add( 'payment-error-message' ); // Add a CSS class for styling 432 errorMessage.textContent = message; 433 434 // Append error message to the container 435 paymentStatusContainer.appendChild( errorMessage ); 436 437 // Auto-clear error message after 10 seconds 438 setTimeout( () => { 439 if ( paymentStatusContainer.contains( errorMessage ) ) { 440 paymentStatusContainer.removeChild( errorMessage ); 441 } 442 }, 10000 ); 443 } else { 444 console.error( 'Payment status container not found.' ); 445 } 446} 447 448function buildVerificationDetails( billingData, token, total ) { 449 return { 450 intent: 'CHARGE', 451 amount: total, 452 currencyCode: SquareConfig.currency, 453 billingContact: billingData, // Use the prepared billing contact details 454 token: token, 455 }; 456} 457 458function clearStoredTokens() { 459 // Remove hidden token fields if they exist 460 jQuery( 'input[name="square_payment_token"]' ).remove(); 461 jQuery( 'input[name="square_verification_token"]' ).remove(); 462} 463 464// Clear cache on checkout update 465jQuery( document.body ).on( 'updated_checkout', function () { 466 ajaxRequestCache = {}; 467 lastPaymentRequestHash = null; 468} ); 469 470/** 471 * Returns the current payment request grand total as a string (e.g. "42.50"). 472 * 473 * This is the SAME VAT-inclusive total the Square paymentRequest was built from 474 * (and that Afterpay/Clearpay creates its checkout with), so it must be used as 475 * the verifyBuyer CHARGE amount. Using a stale/zero value causes Square to throw 476 * "Amount on the request does not match the checkout amount" at completion. 477 * 478 * Reads the cached server payment-request JSON populated by getPaymentRequest(); 479 * falls back to a fresh fetch, then to SquareConfig.orderTotal. 480 * 481 * @return {Promise<string>} The grand total amount, formatted to 2 decimals. 482 */ 483async function getPaymentRequestTotal() { 484 try { 485 const json = await getPaymentRequest(); 486 const obj = typeof json === 'string' ? JSON.parse( json ) : json; 487 const amount = obj && obj.total && obj.total.amount; 488 if ( amount !== undefined && amount !== null && amount !== '' ) { 489 return String( amount ); 490 } 491 } catch ( e ) { 492 console.error( 'Could not read payment request total:', e ); 493 } 494 return SquareConfig.orderTotal || '0.00'; 495} 496 497// Expose functions to global scope for wallets.js to access 498window.handleShippingAddressChanged = handleShippingAddressChanged; 499window.handleShippingOptionChanged = handleShippingOptionChanged; 500window.handleAfterpayShippingAddressChanged = 501 handleAfterpayShippingAddressChanged; 502window.handleAfterpayShippingOptionChanged = 503 handleAfterpayShippingOptionChanged; 504window.initPaymentRequest = initPaymentRequest; 505window.recalculateTotals = recalculateTotals; 506window.checkNeedsShipping = checkNeedsShipping; 507window.getPaymentRequestTotal = getPaymentRequestTotal;
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.