PageSourceSearch

https://themuseumschool.org/wp-content/mu-plugins/vendor/godaddy/m…/assets/js/payments/frontend/wallets.js

js themuseumschool.org collected 2026-10-01 14:59:44 UTC 21,792 bytes, 835 lines download raw bytes

1jQuery(($) => {
2
3	'use strict';
4
5	/**
6	 * Wallets handler.
7	 *
8	 * Interacts with the Poynt Collect API to process payments using Apple and Google Pay.
9	 */
10	window.MWCPaymentsWalletsHandler = class MWCPaymentsWalletsHandler {
11
12		/** @var string flag to display button on cart page */
13		BUTTON_PAGE_CART = 'CART';
14
15		/** @var string flag to display button on checkout page */
16		BUTTON_PAGE_CHECKOUT = 'CHECKOUT';
17
18		/** @var string flag to display button on single product pages */
19		BUTTON_PAGE_SINGLE_PRODUCT = 'SINGLE_PRODUCT';
20
21		/**
22		 * Instantiates the Wallet on the current page.
23		 *
24		 * @param {Object} args form handler arguments
25		 */
26		constructor( args ) {
27
28			this.appId            = args.appId;
29			this.businessId       = args.businessId;
30			this.isLoggingEnabled = args.isLoggingEnabled;
31			this.apiUrl           = args.apiUrl;
32			this.apiNonce         = args.apiNonce;
33			this.initialized      = false;
34			this.enabledButtons   = args.enabledButtons;
35
36			// bail if no Wallet wrappers or enabled buttons exist on the page
37			if (! $('#mwc-payments-wallet-buttons').length || ! Object.keys(this.enabledButtons).length) {
38				return;
39			}
40
41			this.setup()
42		}
43
44		/**
45		 * Sets up the Wallet context and page handlers
46		 */
47		setup() {
48			if ($('form.cart').length) {
49				this.context = this.BUTTON_PAGE_SINGLE_PRODUCT
50				this.uiElement = $('form.cart');
51				this.handleProductPage();
52			} else if ($('form.woocommerce-cart-form').length) {
53				this.context = this.BUTTON_PAGE_CART
54				this.uiElement = $('form.woocommerce-cart-form').parents('div.woocommerce');
55				this.handleCartPage();
56			} else if ($('form.woocommerce-checkout').length) {
57				this.context = this.BUTTON_PAGE_CHECKOUT
58				this.uiElement = $('form.woocommerce-checkout');
59				this.handleCheckoutPage();
60			} else {
61				this.debugLog('No payment form available');
62			}
63		}
64
65		/**
66		 * Determines whether we're currently on a single product page.
67		 *
68		 * @return boolean
69		 */
70		isProductPage() {
71			return this.context === this.BUTTON_PAGE_SINGLE_PRODUCT
72		}
73
74		/**
75		 * Determines whether we're currently on the cart page.
76		 *
77		 * @return boolean
78		 */
79		isCartPage() {
80			return this.context === this.BUTTON_PAGE_CART
81		}
82
83		/**
84		 * Determines whether we're currently on the checkout page.
85		 *
86		 * @return boolean
87		 */
88		isCheckoutPage() {
89			return this.context === this.BUTTON_PAGE_CHECKOUT
90		}
91
92		/**
93		 * Handles setting up Wallet on the single product pages.
94		 */
95		handleProductPage() {
96			this.debugLog('Initializing the product page');
97
98			// the initial payment request update is a dummy promise that resolves instantly
99			this.pendingPaymentRequestUpdate = new Promise((resolve => resolve()))
100
101			if (this.uiElement.hasClass('variations_form')) {
102				this.handleVariableProductPage();
103			} else {
104				let products = this.getCurrentPageProducts()
105
106				if (products.length) {
107					this.initialize(products)
108				}
109
110				this.listenToProductQuantityChanges();
111			}
112		}
113
114		/**
115		 * Listens and handles changes to product quantities
116		 */
117		listenToProductQuantityChanges() {
118			this.uiElement.on('focus.mwc_payments_poynt_wallets', '[name^="quantity"]', (event) => {
119				$(event.target).data('previous-quantity', $(event.target).val());
120			})
121
122			this.uiElement.on('change.mwc_payments_poynt_wallets', '[name^="quantity"]', (event) => {
123
124				let previousQuantity = $(event.target).data('previous-quantity');
125				let newQuantity = $(event.target).val();
126
127				// only refresh the wallet request if the new quantity does not match the previous quantity
128				if (newQuantity === previousQuantity) {
129					return;
130				}
131
132				$(event.target).data('previous-quantity', newQuantity);
133
134				let products = this.getCurrentPageProducts()
135
136				if (products.length) {
137					this.showUI()
138					this.pendingWalletRequestUpdate = this.getPaymentRequest(products)
139				} else {
140					this.hideUI()
141				}
142			})
143
144		}
145
146		/**
147		 * Stops listening to changes to product quantities
148		 */
149		stopListeningToProductQuantityChanges() {
150			this.uiElement.off('change.mwc_payments_poynt_wallets', '[name^="quantity"]');
151			this.uiElement.off('focus.mwc_payments_poynt_wallets', '[name^="quantity"]')
152		}
153
154		/**
155		 * Handles setting up Wallet on the single product page for variable products.
156		 */
157		handleVariableProductPage() {
158			this.uiElement.on('show_variation', (event, variation, purchasable) => {
159				if (purchasable) {
160					this.reInitialize(this.getCurrentPageProducts());
161					this.listenToProductQuantityChanges()
162				}
163			});
164			this.uiElement.on('hide_variation', () => {
165				this.tearDown();
166				this.stopListeningToProductQuantityChanges()
167			});
168		}
169
170		/**
171		 * Handles wallet button click on single product page.
172		 *
173		 * Adds the current product(s) to the cart on single product pages.
174		 *
175		 * @param {Object} event
176		 */
177		handleProductPageWalletButtonClick(event) {
178
179			this.blockUI()
180
181			// Start the wallet session as soon as the pending payment request update resolves. If it's already resolved,
182			// the payment sheet will open immediately.
183			this.pendingPaymentRequestUpdate.then((paymentRequest = {}) => {
184
185				this.startWalletSession(event.source, paymentRequest)
186
187				this.updateCart({
188					products: this.getCurrentPageProducts(),
189				}).then(() => {
190					this.debugLog('Cart updated')
191
192					// The payment request should already be up-to-date by this point, so we don't need to update it here.
193
194				}).catch(err => {
195					this.debugLog('Failed to update cart', err)
196					this.handleApiError(err, event)
197				});
198			}).finally(() => {
199				this.unblockUI()
200			})
201
202		}
203
204		/**
205		 * Gets a list of products with quantities on the current page, ready to be added to cart.
206		 */
207		getCurrentPageProducts() {
208			let products = [];
209
210			let addProduct = (productId, quantity) => {
211
212				if (! productId || isNaN(quantity) || quantity <= 0) {
213					return;
214				}
215
216				products.push({
217					id: productId,
218					quantity: quantity,
219				});
220			}
221
222			// handle grouped quantity inputs
223			if (this.uiElement.hasClass('grouped_form')) {
224
225				this.uiElement.find('input[name^="quantity"]').each((event, element) => {
226					addProduct(
227						parseInt($(element).attr('name').match(/[0-9]+/)),
228						parseFloat($(element).val())
229					)
230				});
231
232				// handle simple & variable products
233			} else {
234
235				addProduct(
236					parseInt(this.uiElement.find('input[name="variation_id"]').val() || this.uiElement.find('button[name="add-to-cart"]').val()),
237					parseFloat(this.uiElement.find('input[name="quantity"]').val())
238				)
239			}
240
241			return products
242		}
243
244		/**
245		 * Handles setting up Wallet on the cart page.
246		 */
247		handleCartPage() {
248			this.debugLog('Initializing the cart page');
249
250			this.initialize();
251
252			$( document.body ).on('updated_cart_totals', () => this.reInitialize());
253		}
254
255		/**
256		 * Handles setting up Wallet on the checkout page.
257		 */
258		handleCheckoutPage() {
259			this.debugLog('Initializing the checkout page');
260
261			$( document.body ).on('updated_checkout', () => this.reInitialize());
262		}
263
264		/**
265		 * Initializes Wallet.
266		 */
267		initialize(products = null) {
268
269			if (this.initializing) {
270				return;
271			}
272
273			this.initializing = true;
274
275			this.getPaymentRequest(products).then((paymentRequest) => {
276
277				this.collect = new TokenizeJs(this.businessId, this.appId, paymentRequest);
278
279				this.collect.supportWalletPayments().then(result => {
280
281					const supportedWallets = Object.keys(this.enabledButtons).filter(walletId => result[this.convertStringToCamelCase(walletId)])
282					const unSupportedWallets = Object.keys(this.enabledButtons).filter(walletId => !result[this.convertStringToCamelCase(walletId)])
283
284					if (supportedWallets.length) {
285
286						this.debugLog(`${supportedWallets.map(this.convertSnakeCaseToCapitalizedString).join(', ')} supported, mounting...`);
287
288						this.collect.mount('mwc-payments-wallet-buttons', document, {
289							paymentMethods: supportedWallets,
290							buttonOptions: {
291								onClick: event => this.handleWalletButtonClick(event)
292							},
293							applePayButtonOptions: this.enabledButtons['apple_pay'] ?? {},
294							googlePayButtonOptions: this.enabledButtons['google_pay'] ?? {},
295						});
296					}
297
298					if (unSupportedWallets.length) {
299
300						this.debugLog(`${unSupportedWallets.map(this.convertSnakeCaseToCapitalizedString).join(', ')} not supported.`);
301
302						if (!supportedWallets.length) {
303							this.hideUI();
304						}
305					}
306
307				});
308
309				this.initializeListeners();
310
311			}).catch((data) => {
312
313				this.debugLog('Could not load payment request', data);
314
315				this.initializing = false;
316			});
317		}
318
319		/**
320		 * Initializes all of the event listeners.
321		 */
322		initializeListeners() {
323
324			// fires when Wallet is ready
325			this.collect.on('ready', event => {
326				this.handleReady(event);
327			} );
328
329			// fires when the Wallet shipping address has been changed
330			this.collect.on('shipping_address_change', event => {
331				this.handleShippingAddressChanged(event);
332			} );
333
334			// fires when the Wallet payment method has been changed
335			this.collect.on('payment_method_change', event => {
336				this.handlePaymentMethodChange(event);
337			} );
338
339			// fires when the Wallet shipping method has been changed
340			this.collect.on('shipping_method_change', event => {
341				this.handleShippingMethodChange(event);
342			} );
343
344			// fires when the Wallet coupon code has been changed
345			this.collect.on('coupon_code_change', event => {
346				this.handleCouponCodeChange(event);
347			} );
348
349			// fires when Wallet has been authorized
350			this.collect.on('payment_authorized', event => {
351				this.handlePaymentAuthorized(event);
352			} );
353
354			// fires when there is an error
355			this.collect.on('error', error => {
356				this.handleError(error);
357			} );
358
359			// fires when the wallet is closed
360			this.collect.on('close_wallet', event => {
361				this.debugLog('Wallet closed', event)
362			} );
363		}
364
365		/**
366		 * Tears down Wallet.
367		 */
368		tearDown() {
369
370			if (this.initialized) {
371				this.collect.unmount('mwc-payments-wallet-buttons', document);
372				this.initialized = false;
373			}
374
375			this.hideUI();
376		}
377
378		/**
379		 * Re-initializes Wallet.
380		 */
381		reInitialize(products = null) {
382
383			this.tearDown();
384
385			if (this.businessId && this.appId && ! this.initializing) {
386				this.initialize(products);
387			}
388		}
389
390		/**
391		 * Handles the "ready" event.
392		 *
393		 * @param {Object} event
394		 */
395		handleReady(event) {
396			this.initializing = false;
397			this.initialized  = true;
398
399			this.debugLog('Wallet is ready', event);
400
401			this.showUI();
402		}
403
404		/**
405		 * Handles the wallet button onClick event.
406		 *
407		 * @param {Object} event
408		 */
409		handleWalletButtonClick(event) {
410			this.debugLog(`${this.convertSnakeCaseToCapitalizedString(event.source)} button clicked`, event);
411
412			// When customer opens the Wallet button on a single product page, the product is not in the cart, yet. We
413			// need to update the cart with the product on the current page as soon as the wallet sheet opens to ensure
414			// when the payment is authorized, the session cart includes the product currently being viewed/purchased.
415			// Note that this operation replaces any previous items in cart.
416			if (this.isProductPage()) {
417				this.handleProductPageWalletButtonClick(event)
418			} else {
419				this.startWalletSession(event.source)
420			}
421		}
422
423		/**
424		 * Starts the wallet session for the given wallet, optionally using a payment request
425		 *
426		 * @param walletId
427		 * @param paymentRequest
428		 */
429		startWalletSession(walletId, paymentRequest = {}) {
430			this.collect[{
431				apple_pay: 'startApplePaySession',
432				google_pay: 'startGooglePaySession'
433			}[walletId]](paymentRequest)
434		}
435
436		/**
437		 * Handles the "shipping_address_change" event.
438		 *
439		 * @param {Object} event
440		 */
441		handleShippingAddressChanged(event) {
442			this.debugLog('The shipping address has been changed', event);
443
444			this.updateCart({
445				customer: {
446					shippingAddress: this.getAdaptedAddress(event.shippingAddress)
447				}
448			}).then(() => {
449				this.debugLog('Cart updated')
450
451				this.getPaymentRequest().then((paymentRequest) => {
452
453					this.debugLog('Payment request updated', paymentRequest)
454
455					event.updateWith(paymentRequest)
456				})
457			}).catch(err => {
458				this.debugLog('Failed to update cart', err)
459				this.handleApiError(err, event);
460			})
461		}
462
463		/**
464		 * Handles API errors.
465		 *
466		 * @param {*} err
467		 * @param {Object} event
468		 */
469		handleApiError(err, event) {
470
471			const data = {
472				error: {
473					message: err.message
474				}
475			}
476			const errCode = err.code.toLowerCase() // error codes must use lowercase snake_case
477
478			// Pass errors to Apple Pay or Google Pay if possible
479			if ([
480				"invalid_shipping_address",
481				"invalid_billing_address",
482				"invalid_coupon_code",
483				"expired_coupon_code",
484				"unserviceable_address",
485				"unknown"
486			].indexOf(errCode) >= 0) {
487				data.error.code = errCode
488
489				if (err.data?.field && ['INVALID_BILLING_ADDRESS', 'INVALID_SHIPPING_ADDRESS'].indexOf(err.code) > -1) {
490					// contrary to the error.code field above, Poynt expects the contactField in camelCase
491					data.error.contactField = this.convertStringToCamelCase(err.data.field)
492				}
493
494				// otherwise, render a generic error and pass unknown error to Apple Pay or Google Pay
495			} else {
496				this.collect.abortApplePaySession()
497
498				this.renderErrors([err.message])
499
500				return;
501			}
502
503			event.complete ? event.complete(data) : event.updateWith(data)
504		}
505
506		/**
507		 * Handles the "payment_method_change" event.
508		 *
509		 * @param {Object} event
510		 */
511		handlePaymentMethodChange(event) {
512			this.debugLog('The payment method has been changed', event);
513
514			// Currently, this method is a no-op, but we still have to call event.updateWith with an empty object to
515			// avoid Apple Pay UI timing out.
516			event.updateWith({});
517		}
518
519		/**
520		 * Handles the "shipping_method_change" event.
521		 *
522		 * @param {Object} event
523		 */
524		handleShippingMethodChange(event) {
525			this.debugLog('The shipping method has been changed', event);
526
527			this.updateCart({
528				customer: {
529					shippingMethod: event.shippingMethod.id
530				}
531			}).then(() => {
532				this.debugLog('Cart updated')
533
534				this.getPaymentRequest().then((paymentRequest) => {
535
536					this.debugLog('Payment request updated', paymentRequest)
537
538					event.updateWith(paymentRequest)
539				})
540			}).catch(err => {
541				this.debugLog('Failed to update cart', err)
542				this.handleApiError(err, event)
543			})
544		}
545
546		/**
547		 * Handles the "coupon_code_change" event.
548		 *
549		 * @param {Object} event
550		 */
551		handleCouponCodeChange(event) {
552			this.debugLog('The coupon code has been changed', event);
553
554			this.updateCart({
555				couponCode: event.couponCode || ''
556			}).then(() => {
557				this.debugLog('Cart updated')
558
559				this.getPaymentRequest().then((paymentRequest) => {
560
561					this.debugLog('Payment request updated', paymentRequest)
562
563					// If the coupon was removed, we need to pass in an empty coupon object here, so that Poynt Collect can remove it.
564					// TODO: refactor this once the bug in Poynt Collect is fixed {@itambek 2022-11-11}
565					if (!paymentRequest.couponCode) {
566						paymentRequest.couponCode = {
567							code: "",
568							label: "",
569							amount: "0.00",
570						}
571					}
572
573					event.updateWith(paymentRequest)
574				})
575			}).catch(err => {
576				this.debugLog('Failed to update cart', err)
577				this.handleApiError(err, event)
578			})
579		}
580
581		/**
582		 * Handles the "payment_authorized" event.
583		 *
584		 * @param {Object} event
585		 */
586		handlePaymentAuthorized(event) {
587			this.debugLog('Payment has been authorized', event);
588
589			let data = {
590				billingAddress: this.getAdaptedAddress(event.billingAddress),
591				shippingAddress: this.getAdaptedAddress(event.shippingAddress),
592			};
593
594			let emailAddress = event.shippingAddress?.emailAddress || event.billingAddress?.emailAddress
595
596			// set the email address (only if it's available - otherwise we might overwrite a valid email address already present in cart)
597			if (emailAddress) {
598				data.emailAddress = emailAddress
599			}
600
601			// Poynt provides the phone number as a part of shipping address, whereas WooCommerce expects it as part of
602			// billing address, so we copy it from shippingAddress to billingAddress
603			if (data.shippingAddress?.phone && !data.billingAddress?.phone) {
604				data.billingAddress.phone = data.shippingAddress.phone
605			}
606
607			this.updateCart({
608				customer: data
609			}).then(() => {
610				this.debugLog('Cart updated')
611
612				this.makeApiRequest('POST', 'payments/godaddy-payments/wallets/processing/pay', {
613					nonce: event.nonce,
614					source: event.source,
615				}).then(res => {
616					this.debugLog('Payment created', res)
617
618					event.complete();
619
620					window.location.replace(res.redirectUrl)
621				}).catch(err => {
622					this.debugLog('Failed to create payment', err)
623					this.handleApiError(err, event)
624				})
625			}).catch(err => {
626				this.debugLog('Failed to update cart', err)
627				this.handleApiError(err, event)
628			})
629		}
630
631		/**
632		 * Handles the error event data.
633		 *
634		 * Logs errors to console and maybe renders them in a user-facing notice.
635		 *
636		 * @param {Object} event after a form error
637		 */
638		handleError(event) {
639
640			this.debugLog('Wallet error', event);
641
642			let errorMessage = poyntPaymentFormI18n.errorMessages.genericError;
643
644			// Poynt Collect API has some inconsistency about error message response data:
645			if ( 'error' === event.type && event.data ) {
646				if ( event.data.error && event.data.error.message && event.data.error.message.message ) {
647					errorMessage = event.data.error.message.message;
648				} else if ( event.data.message ) {
649					errorMessage = event.data.message;
650				} else if ( event.data.error && event.data.error.message && event.data.error.source && 'submit' === event.data.error.source ) {
651					errorMessage = event.data.error.message;
652				} else if ( event.data.error ) {
653					errorMessage = event.data.error;
654				}
655			}
656
657			if (errorMessage.includes('Request failed')) {
658				errorMessage = poyntPaymentFormI18n.errorMessages.genericError;
659			}
660
661			this.renderErrors([errorMessage])
662		}
663
664		/**
665		 * Logs an item to console if logging is enabled.
666		 *
667		 * @param {String} message
668		 * @param {Object|null} data
669		 */
670		debugLog(message, data = null) {
671
672			if (! this.isLoggingEnabled) {
673				return;
674			}
675
676			console.log('[Wallets] '+message);
677
678			if (null !== data) {
679				console.log(data);
680			}
681		}
682
683		/**
684		 * Renders errors to the customer.
685		 *
686		 * @param errors
687		 */
688		renderErrors(errors) {
689			$( '.woocommerce-error, .woocommerce-message' ).remove();
690
691			this.uiElement.prepend('<ul class="woocommerce-error"><li>' + errors.join( '</li><li>' ) + '</li></ul>');
692			this.uiElement.removeClass( 'processing' ).unblock();
693
694			$('html, body').animate({scrollTop: this.uiElement.offset().top - 100}, 1000);
695		}
696
697		/**
698		 * Hides the Wallet UI.
699		 */
700		hideUI() {
701			// NOTE: add more advanced handling when multiple buttons are supported
702			$('.mwc-external-checkout-buttons, .mwc-external-checkout-buttons-divider').hide().removeClass('available');
703		}
704
705		/**
706		 * Shows the Wallet UI.
707		 */
708		showUI() {
709			// NOTE: add more advanced handling when multiple buttons are supported
710			$('.mwc-external-checkout-buttons, .mwc-external-checkout-buttons-divider').show().css('display', 'block').addClass('available');
711		}
712
713		/**
714		 * Blocks the current UI element
715		 */
716		blockUI() {
717			this.uiElement.block({
718				message: null,
719				overlayCSS: {
720					background: '#fff',
721					opacity: 0.6,
722				}
723			});
724		}
725
726		/**
727		 * Unblocks the current UI element
728		 */
729		unblockUI() {
730			this.uiElement.unblock();
731		}
732
733		/**
734		 * Gets the initial payment request.
735		 *
736		 * @returns Promise
737		 */
738		getPaymentRequest(products = null) {
739			return this.makeApiRequest('GET', 'payments/godaddy-payments/wallets/request', products ? { products } : null);
740		}
741
742		/**
743		 * Updates the WooCommerce cart with the given data.
744		 *
745		 * @returns Promise
746		 */
747		updateCart(data) {
748			return this.makeApiRequest('PATCH', 'cart', data);
749		}
750
751		/**
752		 * Makes a request to the site REST API.
753		 *
754		 * @param {String} method
755		 * @param {String} route
756		 * @param {Object} data
757		 *
758		 * @return Promise
759		 */
760		makeApiRequest(method = 'GET', route, data = {}) {
761
762			return new Promise((resolve, reject) => {
763				$.ajax({
764					url: this.apiUrl+'godaddy/mwc/v1/'+route,
765					dataType: 'json',
766					method: method,
767					headers: {
768						'X-MWC-Payments-Nonce': this.apiNonce
769					},
770					data: data
771				}).done((data) => {
772					resolve(data);
773				}).fail((jqXHR) => {
774					reject(jqXHR.responseJSON);
775				});
776			});
777		}
778
779
780		/**
781		 * Converts a string to camelCase
782		 *
783		 * @param {String} str
784		 *
785		 * @return String
786		 */
787		convertStringToCamelCase = str => {
788			return str.toLowerCase().replace(/[-_][a-z0-9]/g, group =>
789				group.toUpperCase()
790					.replace('-', '')
791					.replace('_', '')
792			);
793		};
794
795
796		/**
797		 * Converts snake_case to capitalized string
798		 *
799		 * @param {String} str
800		 *
801		 * @return String
802		 */
803		convertSnakeCaseToCapitalizedString = str => {
804			return str.split('_').map((word) => {
805				return word[0].toUpperCase() + word.substring(1);
806			}).join(' ');
807		};
808
809
810		/**
811		 * Converts the address provided by Wallet to the format required by GDP.
812		 *
813		 * @param {Object} address
814		 * @see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentcontact
815		 *
816		 * @return Object
817		 */
818		getAdaptedAddress(address) {
819			return address ? {
820				countryCode: address.countryCode,
821				locality: address.locality,
822				postalCode: address.postalCode,
823				firstName: address.givenName ?? address.name?.split(' ')[0],
824				lastName: address.familyName ?? address.name?.split(' ').slice(1).join(' '),
825				lines: address.addressLines,
826				phone: address.phoneNumber,
827				administrativeDistricts: [address.administrativeArea, address.subAdministrativeArea],
828			} : null
829		}
830	}
831
832	// dispatch loaded event
833	$( document.body ).trigger( 'mwc_payments_wallets_handler_loaded' );
834
835} );

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.