HEX
Server: Apache
System: Linux efa57bbe-abb1-400d-2985-3b056fbc2701.secureserver.net 6.1.147-1.el9.elrepo.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Jul 24 12:33:32 EDT 2025 x86_64
User: root (0)
PHP: 8.0.30.4
Disabled: NONE
Upload Files
File: //var/www/wp-content/mu-plugins/vendor/godaddy/mwc-core/assets/js/payments/frontend/wallets.js
jQuery(($) => {

	'use strict';

	/**
	 * Wallets handler.
	 *
	 * Interacts with the Poynt Collect API to process payments using Apple and Google Pay.
	 */
	window.MWCPaymentsWalletsHandler = class MWCPaymentsWalletsHandler {

		/** @var string flag to display button on cart page */
		BUTTON_PAGE_CART = 'CART';

		/** @var string flag to display button on checkout page */
		BUTTON_PAGE_CHECKOUT = 'CHECKOUT';

		/** @var string flag to display button on single product pages */
		BUTTON_PAGE_SINGLE_PRODUCT = 'SINGLE_PRODUCT';

		/**
		 * Instantiates the Wallet on the current page.
		 *
		 * @param {Object} args form handler arguments
		 */
		constructor( args ) {

			this.appId            = args.appId;
			this.businessId       = args.businessId;
			this.isLoggingEnabled = args.isLoggingEnabled;
			this.apiUrl           = args.apiUrl;
			this.apiNonce         = args.apiNonce;
			this.initialized      = false;
			this.enabledButtons   = args.enabledButtons;

			// bail if no Wallet wrappers or enabled buttons exist on the page
			if (! $('#mwc-payments-wallet-buttons').length || ! Object.keys(this.enabledButtons).length) {
				return;
			}

			this.setup()
		}

		/**
		 * Sets up the Wallet context and page handlers
		 */
		setup() {
			if ($('form.cart').length) {
				this.context = this.BUTTON_PAGE_SINGLE_PRODUCT
				this.uiElement = $('form.cart');
				this.handleProductPage();
			} else if ($('form.woocommerce-cart-form').length) {
				this.context = this.BUTTON_PAGE_CART
				this.uiElement = $('form.woocommerce-cart-form').parents('div.woocommerce');
				this.handleCartPage();
			} else if ($('form.woocommerce-checkout').length) {
				this.context = this.BUTTON_PAGE_CHECKOUT
				this.uiElement = $('form.woocommerce-checkout');
				this.handleCheckoutPage();
			} else {
				this.debugLog('No payment form available');
			}
		}

		/**
		 * Determines whether we're currently on a single product page.
		 *
		 * @return boolean
		 */
		isProductPage() {
			return this.context === this.BUTTON_PAGE_SINGLE_PRODUCT
		}

		/**
		 * Determines whether we're currently on the cart page.
		 *
		 * @return boolean
		 */
		isCartPage() {
			return this.context === this.BUTTON_PAGE_CART
		}

		/**
		 * Determines whether we're currently on the checkout page.
		 *
		 * @return boolean
		 */
		isCheckoutPage() {
			return this.context === this.BUTTON_PAGE_CHECKOUT
		}

		/**
		 * Handles setting up Wallet on the single product pages.
		 */
		handleProductPage() {
			this.debugLog('Initializing the product page');

			// the initial payment request update is a dummy promise that resolves instantly
			this.pendingPaymentRequestUpdate = new Promise((resolve => resolve()))

			if (this.uiElement.hasClass('variations_form')) {
				this.handleVariableProductPage();
			} else {
				let products = this.getCurrentPageProducts()

				if (products.length) {
					this.initialize(products)
				}

				this.listenToProductQuantityChanges();
			}
		}

		/**
		 * Listens and handles changes to product quantities
		 */
		listenToProductQuantityChanges() {
			this.uiElement.on('focus.mwc_payments_poynt_wallets', '[name^="quantity"]', (event) => {
				$(event.target).data('previous-quantity', $(event.target).val());
			})

			this.uiElement.on('change.mwc_payments_poynt_wallets', '[name^="quantity"]', (event) => {

				let previousQuantity = $(event.target).data('previous-quantity');
				let newQuantity = $(event.target).val();

				// only refresh the wallet request if the new quantity does not match the previous quantity
				if (newQuantity === previousQuantity) {
					return;
				}

				$(event.target).data('previous-quantity', newQuantity);

				let products = this.getCurrentPageProducts()

				if (products.length) {
					this.showUI()
					this.pendingWalletRequestUpdate = this.getPaymentRequest(products)
				} else {
					this.hideUI()
				}
			})

		}

		/**
		 * Stops listening to changes to product quantities
		 */
		stopListeningToProductQuantityChanges() {
			this.uiElement.off('change.mwc_payments_poynt_wallets', '[name^="quantity"]');
			this.uiElement.off('focus.mwc_payments_poynt_wallets', '[name^="quantity"]')
		}

		/**
		 * Handles setting up Wallet on the single product page for variable products.
		 */
		handleVariableProductPage() {
			this.uiElement.on('show_variation', (event, variation, purchasable) => {
				if (purchasable) {
					this.reInitialize(this.getCurrentPageProducts());
					this.listenToProductQuantityChanges()
				}
			});
			this.uiElement.on('hide_variation', () => {
				this.tearDown();
				this.stopListeningToProductQuantityChanges()
			});
		}

		/**
		 * Handles wallet button click on single product page.
		 *
		 * Adds the current product(s) to the cart on single product pages.
		 *
		 * @param {Object} event
		 */
		handleProductPageWalletButtonClick(event) {

			this.blockUI()

			// Start the wallet session as soon as the pending payment request update resolves. If it's already resolved,
			// the payment sheet will open immediately.
			this.pendingPaymentRequestUpdate.then((paymentRequest = {}) => {

				this.startWalletSession(event.source, paymentRequest)

				this.updateCart({
					products: this.getCurrentPageProducts(),
				}).then(() => {
					this.debugLog('Cart updated')

					// The payment request should already be up-to-date by this point, so we don't need to update it here.

				}).catch(err => {
					this.debugLog('Failed to update cart', err)
					this.handleApiError(err, event)
				});
			}).finally(() => {
				this.unblockUI()
			})

		}

		/**
		 * Gets a list of products with quantities on the current page, ready to be added to cart.
		 */
		getCurrentPageProducts() {
			let products = [];

			let addProduct = (productId, quantity) => {

				if (! productId || isNaN(quantity) || quantity <= 0) {
					return;
				}

				products.push({
					id: productId,
					quantity: quantity,
				});
			}

			// handle grouped quantity inputs
			if (this.uiElement.hasClass('grouped_form')) {

				this.uiElement.find('input[name^="quantity"]').each((event, element) => {
					addProduct(
						parseInt($(element).attr('name').match(/[0-9]+/)),
						parseFloat($(element).val())
					)
				});

				// handle simple & variable products
			} else {

				addProduct(
					parseInt(this.uiElement.find('input[name="variation_id"]').val() || this.uiElement.find('button[name="add-to-cart"]').val()),
					parseFloat(this.uiElement.find('input[name="quantity"]').val())
				)
			}

			return products
		}

		/**
		 * Handles setting up Wallet on the cart page.
		 */
		handleCartPage() {
			this.debugLog('Initializing the cart page');

			this.initialize();

			$( document.body ).on('updated_cart_totals', () => this.reInitialize());
		}

		/**
		 * Handles setting up Wallet on the checkout page.
		 */
		handleCheckoutPage() {
			this.debugLog('Initializing the checkout page');

			$( document.body ).on('updated_checkout', () => this.reInitialize());
		}

		/**
		 * Initializes Wallet.
		 */
		initialize(products = null) {

			if (this.initializing) {
				return;
			}

			this.initializing = true;

			this.getPaymentRequest(products).then((paymentRequest) => {

				this.collect = new TokenizeJs(this.businessId, this.appId, paymentRequest);

				this.collect.supportWalletPayments().then(result => {

					const supportedWallets = Object.keys(this.enabledButtons).filter(walletId => result[this.convertStringToCamelCase(walletId)])
					const unSupportedWallets = Object.keys(this.enabledButtons).filter(walletId => !result[this.convertStringToCamelCase(walletId)])

					if (supportedWallets.length) {

						this.debugLog(`${supportedWallets.map(this.convertSnakeCaseToCapitalizedString).join(', ')} supported, mounting...`);

						this.collect.mount('mwc-payments-wallet-buttons', document, {
							paymentMethods: supportedWallets,
							buttonOptions: {
								onClick: event => this.handleWalletButtonClick(event)
							},
							applePayButtonOptions: this.enabledButtons['apple_pay'] ?? {},
							googlePayButtonOptions: this.enabledButtons['google_pay'] ?? {},
						});
					}

					if (unSupportedWallets.length) {

						this.debugLog(`${unSupportedWallets.map(this.convertSnakeCaseToCapitalizedString).join(', ')} not supported.`);

						if (!supportedWallets.length) {
							this.hideUI();
						}
					}

				});

				this.initializeListeners();

			}).catch((data) => {

				this.debugLog('Could not load payment request', data);

				this.initializing = false;
			});
		}

		/**
		 * Initializes all of the event listeners.
		 */
		initializeListeners() {

			// fires when Wallet is ready
			this.collect.on('ready', event => {
				this.handleReady(event);
			} );

			// fires when the Wallet shipping address has been changed
			this.collect.on('shipping_address_change', event => {
				this.handleShippingAddressChanged(event);
			} );

			// fires when the Wallet payment method has been changed
			this.collect.on('payment_method_change', event => {
				this.handlePaymentMethodChange(event);
			} );

			// fires when the Wallet shipping method has been changed
			this.collect.on('shipping_method_change', event => {
				this.handleShippingMethodChange(event);
			} );

			// fires when the Wallet coupon code has been changed
			this.collect.on('coupon_code_change', event => {
				this.handleCouponCodeChange(event);
			} );

			// fires when Wallet has been authorized
			this.collect.on('payment_authorized', event => {
				this.handlePaymentAuthorized(event);
			} );

			// fires when there is an error
			this.collect.on('error', error => {
				this.handleError(error);
			} );

			// fires when the wallet is closed
			this.collect.on('close_wallet', event => {
				this.debugLog('Wallet closed', event)
			} );
		}

		/**
		 * Tears down Wallet.
		 */
		tearDown() {

			if (this.initialized) {
				this.collect.unmount('mwc-payments-wallet-buttons', document);
				this.initialized = false;
			}

			this.hideUI();
		}

		/**
		 * Re-initializes Wallet.
		 */
		reInitialize(products = null) {

			this.tearDown();

			if (this.businessId && this.appId && ! this.initializing) {
				this.initialize(products);
			}
		}

		/**
		 * Handles the "ready" event.
		 *
		 * @param {Object} event
		 */
		handleReady(event) {
			this.initializing = false;
			this.initialized  = true;

			this.debugLog('Wallet is ready', event);

			this.showUI();
		}

		/**
		 * Handles the wallet button onClick event.
		 *
		 * @param {Object} event
		 */
		handleWalletButtonClick(event) {
			this.debugLog(`${this.convertSnakeCaseToCapitalizedString(event.source)} button clicked`, event);

			// When customer opens the Wallet button on a single product page, the product is not in the cart, yet. We
			// need to update the cart with the product on the current page as soon as the wallet sheet opens to ensure
			// when the payment is authorized, the session cart includes the product currently being viewed/purchased.
			// Note that this operation replaces any previous items in cart.
			if (this.isProductPage()) {
				this.handleProductPageWalletButtonClick(event)
			} else {
				this.startWalletSession(event.source)
			}
		}

		/**
		 * Starts the wallet session for the given wallet, optionally using a payment request
		 *
		 * @param walletId
		 * @param paymentRequest
		 */
		startWalletSession(walletId, paymentRequest = {}) {
			this.collect[{
				apple_pay: 'startApplePaySession',
				google_pay: 'startGooglePaySession'
			}[walletId]](paymentRequest)
		}

		/**
		 * Handles the "shipping_address_change" event.
		 *
		 * @param {Object} event
		 */
		handleShippingAddressChanged(event) {
			this.debugLog('The shipping address has been changed', event);

			this.updateCart({
				customer: {
					shippingAddress: this.getAdaptedAddress(event.shippingAddress)
				}
			}).then(() => {
				this.debugLog('Cart updated')

				this.getPaymentRequest().then((paymentRequest) => {

					this.debugLog('Payment request updated', paymentRequest)

					event.updateWith(paymentRequest)
				})
			}).catch(err => {
				this.debugLog('Failed to update cart', err)
				this.handleApiError(err, event);
			})
		}

		/**
		 * Handles API errors.
		 *
		 * @param {*} err
		 * @param {Object} event
		 */
		handleApiError(err, event) {

			const data = {
				error: {
					message: err.message
				}
			}
			const errCode = err.code.toLowerCase() // error codes must use lowercase snake_case

			// Pass errors to Apple Pay or Google Pay if possible
			if ([
				"invalid_shipping_address",
				"invalid_billing_address",
				"invalid_coupon_code",
				"expired_coupon_code",
				"unserviceable_address",
				"unknown"
			].indexOf(errCode) >= 0) {
				data.error.code = errCode

				if (err.data?.field && ['INVALID_BILLING_ADDRESS', 'INVALID_SHIPPING_ADDRESS'].indexOf(err.code) > -1) {
					// contrary to the error.code field above, Poynt expects the contactField in camelCase
					data.error.contactField = this.convertStringToCamelCase(err.data.field)
				}

				// otherwise, render a generic error and pass unknown error to Apple Pay or Google Pay
			} else {
				this.collect.abortApplePaySession()

				this.renderErrors([err.message])

				return;
			}

			event.complete ? event.complete(data) : event.updateWith(data)
		}

		/**
		 * Handles the "payment_method_change" event.
		 *
		 * @param {Object} event
		 */
		handlePaymentMethodChange(event) {
			this.debugLog('The payment method has been changed', event);

			// Currently, this method is a no-op, but we still have to call event.updateWith with an empty object to
			// avoid Apple Pay UI timing out.
			event.updateWith({});
		}

		/**
		 * Handles the "shipping_method_change" event.
		 *
		 * @param {Object} event
		 */
		handleShippingMethodChange(event) {
			this.debugLog('The shipping method has been changed', event);

			this.updateCart({
				customer: {
					shippingMethod: event.shippingMethod.id
				}
			}).then(() => {
				this.debugLog('Cart updated')

				this.getPaymentRequest().then((paymentRequest) => {

					this.debugLog('Payment request updated', paymentRequest)

					event.updateWith(paymentRequest)
				})
			}).catch(err => {
				this.debugLog('Failed to update cart', err)
				this.handleApiError(err, event)
			})
		}

		/**
		 * Handles the "coupon_code_change" event.
		 *
		 * @param {Object} event
		 */
		handleCouponCodeChange(event) {
			this.debugLog('The coupon code has been changed', event);

			this.updateCart({
				couponCode: event.couponCode || ''
			}).then(() => {
				this.debugLog('Cart updated')

				this.getPaymentRequest().then((paymentRequest) => {

					this.debugLog('Payment request updated', paymentRequest)

					// If the coupon was removed, we need to pass in an empty coupon object here, so that Poynt Collect can remove it.
					// TODO: refactor this once the bug in Poynt Collect is fixed {@itambek 2022-11-11}
					if (!paymentRequest.couponCode) {
						paymentRequest.couponCode = {
							code: "",
							label: "",
							amount: "0.00",
						}
					}

					event.updateWith(paymentRequest)
				})
			}).catch(err => {
				this.debugLog('Failed to update cart', err)
				this.handleApiError(err, event)
			})
		}

		/**
		 * Handles the "payment_authorized" event.
		 *
		 * @param {Object} event
		 */
		handlePaymentAuthorized(event) {
			this.debugLog('Payment has been authorized', event);

			let data = {
				billingAddress: this.getAdaptedAddress(event.billingAddress),
				shippingAddress: this.getAdaptedAddress(event.shippingAddress),
			};

			let emailAddress = event.shippingAddress?.emailAddress || event.billingAddress?.emailAddress

			// set the email address (only if it's available - otherwise we might overwrite a valid email address already present in cart)
			if (emailAddress) {
				data.emailAddress = emailAddress
			}

			// Poynt provides the phone number as a part of shipping address, whereas WooCommerce expects it as part of
			// billing address, so we copy it from shippingAddress to billingAddress
			if (data.shippingAddress?.phone && !data.billingAddress?.phone) {
				data.billingAddress.phone = data.shippingAddress.phone
			}

			this.updateCart({
				customer: data
			}).then(() => {
				this.debugLog('Cart updated')

				this.makeApiRequest('POST', 'payments/godaddy-payments/wallets/processing/pay', {
					nonce: event.nonce,
					source: event.source,
				}).then(res => {
					this.debugLog('Payment created', res)

					event.complete();

					window.location.replace(res.redirectUrl)
				}).catch(err => {
					this.debugLog('Failed to create payment', err)
					this.handleApiError(err, event)
				})
			}).catch(err => {
				this.debugLog('Failed to update cart', err)
				this.handleApiError(err, event)
			})
		}

		/**
		 * Handles the error event data.
		 *
		 * Logs errors to console and maybe renders them in a user-facing notice.
		 *
		 * @param {Object} event after a form error
		 */
		handleError(event) {

			this.debugLog('Wallet error', event);

			let errorMessage = poyntPaymentFormI18n.errorMessages.genericError;

			// Poynt Collect API has some inconsistency about error message response data:
			if ( 'error' === event.type && event.data ) {
				if ( event.data.error && event.data.error.message && event.data.error.message.message ) {
					errorMessage = event.data.error.message.message;
				} else if ( event.data.message ) {
					errorMessage = event.data.message;
				} else if ( event.data.error && event.data.error.message && event.data.error.source && 'submit' === event.data.error.source ) {
					errorMessage = event.data.error.message;
				} else if ( event.data.error ) {
					errorMessage = event.data.error;
				}
			}

			if (errorMessage.includes('Request failed')) {
				errorMessage = poyntPaymentFormI18n.errorMessages.genericError;
			}

			this.renderErrors([errorMessage])
		}

		/**
		 * Logs an item to console if logging is enabled.
		 *
		 * @param {String} message
		 * @param {Object|null} data
		 */
		debugLog(message, data = null) {

			if (! this.isLoggingEnabled) {
				return;
			}

			console.log('[Wallets] '+message);

			if (null !== data) {
				console.log(data);
			}
		}

		/**
		 * Renders errors to the customer.
		 *
		 * @param errors
		 */
		renderErrors(errors) {
			$( '.woocommerce-error, .woocommerce-message' ).remove();

			this.uiElement.prepend('<ul class="woocommerce-error"><li>' + errors.join( '</li><li>' ) + '</li></ul>');
			this.uiElement.removeClass( 'processing' ).unblock();

			$('html, body').animate({scrollTop: this.uiElement.offset().top - 100}, 1000);
		}

		/**
		 * Hides the Wallet UI.
		 */
		hideUI() {
			// NOTE: add more advanced handling when multiple buttons are supported
			$('.mwc-external-checkout-buttons, .mwc-external-checkout-buttons-divider').hide().removeClass('available');
		}

		/**
		 * Shows the Wallet UI.
		 */
		showUI() {
			// NOTE: add more advanced handling when multiple buttons are supported
			$('.mwc-external-checkout-buttons, .mwc-external-checkout-buttons-divider').show().css('display', 'block').addClass('available');
		}

		/**
		 * Blocks the current UI element
		 */
		blockUI() {
			this.uiElement.block({
				message: null,
				overlayCSS: {
					background: '#fff',
					opacity: 0.6,
				}
			});
		}

		/**
		 * Unblocks the current UI element
		 */
		unblockUI() {
			this.uiElement.unblock();
		}

		/**
		 * Gets the initial payment request.
		 *
		 * @returns Promise
		 */
		getPaymentRequest(products = null) {
			return this.makeApiRequest('GET', 'payments/godaddy-payments/wallets/request', products ? { products } : null);
		}

		/**
		 * Updates the WooCommerce cart with the given data.
		 *
		 * @returns Promise
		 */
		updateCart(data) {
			return this.makeApiRequest('PATCH', 'cart', data);
		}

		/**
		 * Makes a request to the site REST API.
		 *
		 * @param {String} method
		 * @param {String} route
		 * @param {Object} data
		 *
		 * @return Promise
		 */
		makeApiRequest(method = 'GET', route, data = {}) {

			return new Promise((resolve, reject) => {
				$.ajax({
					url: this.apiUrl+'godaddy/mwc/v1/'+route,
					dataType: 'json',
					method: method,
					headers: {
						'X-MWC-Payments-Nonce': this.apiNonce
					},
					data: data
				}).done((data) => {
					resolve(data);
				}).fail((jqXHR) => {
					reject(jqXHR.responseJSON);
				});
			});
		}


		/**
		 * Converts a string to camelCase
		 *
		 * @param {String} str
		 *
		 * @return String
		 */
		convertStringToCamelCase = str => {
			return str.toLowerCase().replace(/[-_][a-z0-9]/g, group =>
				group.toUpperCase()
					.replace('-', '')
					.replace('_', '')
			);
		};


		/**
		 * Converts snake_case to capitalized string
		 *
		 * @param {String} str
		 *
		 * @return String
		 */
		convertSnakeCaseToCapitalizedString = str => {
			return str.split('_').map((word) => {
				return word[0].toUpperCase() + word.substring(1);
			}).join(' ');
		};


		/**
		 * Converts the address provided by Wallet to the format required by GDP.
		 *
		 * @param {Object} address
		 * @see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentcontact
		 *
		 * @return Object
		 */
		getAdaptedAddress(address) {
			return address ? {
				countryCode: address.countryCode,
				locality: address.locality,
				postalCode: address.postalCode,
				firstName: address.givenName ?? address.name?.split(' ')[0],
				lastName: address.familyName ?? address.name?.split(' ').slice(1).join(' '),
				lines: address.addressLines,
				phone: address.phoneNumber,
				administrativeDistricts: [address.administrativeArea, address.subAdministrativeArea],
			} : null
		}
	}

	// dispatch loaded event
	$( document.body ).trigger( 'mwc_payments_wallets_handler_loaded' );

} );