PageSourceSearch

https://savingcranes.org/wp-content/plugins/squarewoosync-pro/assets/js/utils.js?ver=10.3.9.1787846148

js savingcranes.org collected 2026-09-24 09:03:47 UTC 15,883 bytes, 507 lines download raw bytes

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.