Initial commit
diff --git a/assets/js/stripe-payment-request.js b/assets/js/stripe-payment-request.js
new file mode 100644
index 0000000..3b4fc54
--- /dev/null
+++ b/assets/js/stripe-payment-request.js
@@ -0,0 +1,825 @@
+/* global wc_stripe_payment_request_params, Stripe */
+jQuery( function( $ ) {
+	'use strict';
+
+	var stripe = Stripe( wc_stripe_payment_request_params.stripe.key, {
+		locale: wc_stripe_payment_request_params.stripe.locale
+	} ),
+		paymentRequestType;
+
+	/**
+	 * Object to handle Stripe payment forms.
+	 */
+	var wc_stripe_payment_request = {
+		/**
+		 * Get WC AJAX endpoint URL.
+		 *
+		 * @param  {String} endpoint Endpoint.
+		 * @return {String}
+		 */
+		getAjaxURL: function( endpoint ) {
+			return wc_stripe_payment_request_params.ajax_url
+				.toString()
+				.replace( '%%endpoint%%', 'wc_stripe_' + endpoint );
+		},
+
+		getCartDetails: function() {
+			var data = {
+				security: wc_stripe_payment_request_params.nonce.payment
+			};
+
+			$.ajax( {
+				type:    'POST',
+				data:    data,
+				url:     wc_stripe_payment_request.getAjaxURL( 'get_cart_details' ),
+				success: function( response ) {
+					wc_stripe_payment_request.startPaymentRequest( response );
+				}
+			} );
+		},
+
+		getAttributes: function() {
+			var select = $( '.variations_form' ).find( '.variations select' ),
+				data   = {},
+				count  = 0,
+				chosen = 0;
+
+			select.each( function() {
+				var attribute_name = $( this ).data( 'attribute_name' ) || $( this ).attr( 'name' );
+				var value          = $( this ).val() || '';
+
+				if ( value.length > 0 ) {
+					chosen ++;
+				}
+
+				count ++;
+				data[ attribute_name ] = value;
+			});
+
+			return {
+				'count'      : count,
+				'chosenCount': chosen,
+				'data'       : data
+			};
+		},
+
+		processSource: function( source, paymentRequestType ) {
+			var data = wc_stripe_payment_request.getOrderData( source, paymentRequestType );
+
+			return $.ajax( {
+				type:    'POST',
+				data:    data,
+				dataType: 'json',
+				url:     wc_stripe_payment_request.getAjaxURL( 'create_order' )
+			} );
+		},
+
+		/**
+		 * Get order data.
+		 *
+		 * @since 3.1.0
+		 * @version 4.0.0
+		 * @param {PaymentResponse} source Payment Response instance.
+		 *
+		 * @return {Object}
+		 */
+		getOrderData: function( evt, paymentRequestType ) {
+			var source   = evt.source;
+			var email    = source.owner.email;
+			var phone    = source.owner.phone;
+			var billing  = source.owner.address;
+			var name     = source.owner.name;
+			var shipping = evt.shippingAddress;
+			var data     = {
+				_wpnonce:                  wc_stripe_payment_request_params.nonce.checkout,
+				billing_first_name:        null !== name ? name.split( ' ' ).slice( 0, 1 ).join( ' ' ) : '',
+				billing_last_name:         null !== name ? name.split( ' ' ).slice( 1 ).join( ' ' ) : '',
+				billing_company:           '',
+				billing_email:             null !== email   ? email : evt.payerEmail,
+				billing_phone:             null !== phone   ? phone : evt.payerPhone && evt.payerPhone.replace( '/[() -]/g', '' ),
+				billing_country:           null !== billing ? billing.country : '',
+				billing_address_1:         null !== billing ? billing.line1 : '',
+				billing_address_2:         null !== billing ? billing.line2 : '',
+				billing_city:              null !== billing ? billing.city : '',
+				billing_state:             null !== billing ? billing.state : '',
+				billing_postcode:          null !== billing ? billing.postal_code : '',
+				shipping_first_name:       '',
+				shipping_last_name:        '',
+				shipping_company:          '',
+				shipping_country:          '',
+				shipping_address_1:        '',
+				shipping_address_2:        '',
+				shipping_city:             '',
+				shipping_state:            '',
+				shipping_postcode:         '',
+				shipping_method:           [ null === evt.shippingOption ? null : evt.shippingOption.id ],
+				order_comments:            '',
+				payment_method:            'stripe',
+				ship_to_different_address: 1,
+				terms:                     1,
+				stripe_source:             source.id,
+				payment_request_type:      paymentRequestType
+			};
+
+			if ( shipping ) {
+				data.shipping_first_name = shipping.recipient.split( ' ' ).slice( 0, 1 ).join( ' ' );
+				data.shipping_last_name  = shipping.recipient.split( ' ' ).slice( 1 ).join( ' ' );
+				data.shipping_company    = shipping.organization;
+				data.shipping_country    = shipping.country;
+				data.shipping_address_1  = typeof shipping.addressLine[0] === 'undefined' ? '' : shipping.addressLine[0];
+				data.shipping_address_2  = typeof shipping.addressLine[1] === 'undefined' ? '' : shipping.addressLine[1];
+				data.shipping_city       = shipping.city;
+				data.shipping_state      = shipping.region;
+				data.shipping_postcode   = shipping.postalCode;
+			}
+
+			return data;
+		},
+
+		/**
+		 * Generate error message HTML.
+		 *
+		 * @since 3.1.0
+		 * @version 4.0.0
+		 * @param  {String} message Error message.
+		 * @return {Object}
+		 */
+		getErrorMessageHTML: function( message ) {
+			return $( '<div class="woocommerce-error" />' ).text( message );
+		},
+
+		/**
+		 * Display error messages.
+		 *
+		 * @since 4.8.0
+		 * @param {Object} message DOM object with error message to display.
+		 */
+		displayErrorMessage: function( message ) {
+			$( '.woocommerce-error' ).remove();
+
+			if ( wc_stripe_payment_request_params.is_product_page ) {
+				var element = $( '.product' ).first();
+				element.before( message );
+
+				$( 'html, body' ).animate({
+					scrollTop: element.prev( '.woocommerce-error' ).offset().top
+				}, 600 );
+			} else {
+				var $form = $( '.shop_table.cart' ).closest( 'form' );
+				$form.before( message );
+				$( 'html, body' ).animate({
+					scrollTop: $form.prev( '.woocommerce-error' ).offset().top
+				}, 600 );
+			}
+		},
+
+		/**
+		 * Abort payment and display error messages.
+		 *
+		 * @since 3.1.0
+		 * @version 4.8.0
+		 * @param {PaymentResponse} payment Payment response instance.
+		 * @param {Object}          message DOM object with error message to display.
+		 */
+		abortPayment: function( payment, message ) {
+			payment.complete( 'fail' );
+			wc_stripe_payment_request.displayErrorMessage( message );
+		},
+
+		/**
+		 * Complete payment.
+		 *
+		 * @since 3.1.0
+		 * @version 4.0.0
+		 * @param {PaymentResponse} payment Payment response instance.
+		 * @param {String}          url     Order thank you page URL.
+		 */
+		completePayment: function( payment, url ) {
+			wc_stripe_payment_request.block();
+
+			payment.complete( 'success' );
+
+			// Success, then redirect to the Thank You page.
+			window.location = url;
+		},
+
+		block: function() {
+			$.blockUI( {
+				message: null,
+				overlayCSS: {
+					background: '#fff',
+					opacity: 0.6
+				}
+			} );
+		},
+
+		/**
+		 * Update shipping options.
+		 *
+		 * @param {Object}         details Payment details.
+		 * @param {PaymentAddress} address Shipping address.
+		 */
+		updateShippingOptions: function( details, address ) {
+			var data = {
+				security:  wc_stripe_payment_request_params.nonce.shipping,
+				country:   address.country,
+				state:     address.region,
+				postcode:  address.postalCode,
+				city:      address.city,
+				address:   typeof address.addressLine[0] === 'undefined' ? '' : address.addressLine[0],
+				address_2: typeof address.addressLine[1] === 'undefined' ? '' : address.addressLine[1],
+				payment_request_type: paymentRequestType,
+				is_product_page: wc_stripe_payment_request_params.is_product_page,
+			};
+
+			return $.ajax( {
+				type:    'POST',
+				data:    data,
+				url:     wc_stripe_payment_request.getAjaxURL( 'get_shipping_options' )
+			} );
+		},
+
+		/**
+		 * Updates the shipping price and the total based on the shipping option.
+		 *
+		 * @param {Object}   details        The line items and shipping options.
+		 * @param {String}   shippingOption User's preferred shipping option to use for shipping price calculations.
+		 */
+		updateShippingDetails: function( details, shippingOption ) {
+			var data = {
+				security: wc_stripe_payment_request_params.nonce.update_shipping,
+				shipping_method: [ shippingOption.id ],
+				payment_request_type: paymentRequestType,
+				is_product_page: wc_stripe_payment_request_params.is_product_page,
+			};
+
+			return $.ajax( {
+				type: 'POST',
+				data: data,
+				url:  wc_stripe_payment_request.getAjaxURL( 'update_shipping_method' )
+			} );
+		},
+
+		/**
+		 * Adds the item to the cart and return cart details.
+		 *
+		 */
+		addToCart: function() {
+			var product_id = $( '.single_add_to_cart_button' ).val();
+
+			// Check if product is a variable product.
+			if ( $( '.single_variation_wrap' ).length ) {
+				product_id = $( '.single_variation_wrap' ).find( 'input[name="product_id"]' ).val();
+			}
+
+			var data = {
+				security: wc_stripe_payment_request_params.nonce.add_to_cart,
+				product_id: product_id,
+				qty: $( '.quantity .qty' ).val(),
+				attributes: $( '.variations_form' ).length ? wc_stripe_payment_request.getAttributes().data : []
+			};
+
+			// add addons data to the POST body
+			var formData = $( 'form.cart' ).serializeArray();
+			$.each( formData, function( i, field ) {
+				if ( /^addon-/.test( field.name ) ) {
+					if ( /\[\]$/.test( field.name ) ) {
+						var fieldName = field.name.substring( 0, field.name.length - 2);
+						if ( data[ fieldName ] ) {
+							data[ fieldName ].push( field.value );
+						} else {
+							data[ fieldName ] = [ field.value ];
+						}
+					} else {
+						data[ field.name ] = field.value;
+					}
+				}
+			} );
+
+			return $.ajax( {
+				type: 'POST',
+				data: data,
+				url:  wc_stripe_payment_request.getAjaxURL( 'add_to_cart' )
+			} );
+		},
+
+		clearCart: function() {
+			var data = {
+					'security': wc_stripe_payment_request_params.nonce.clear_cart
+				};
+
+			return $.ajax( {
+				type:    'POST',
+				data:    data,
+				url:     wc_stripe_payment_request.getAjaxURL( 'clear_cart' ),
+				success: function( response ) {}
+			} );
+		},
+
+		getRequestOptionsFromLocal: function() {
+			return {
+				total: wc_stripe_payment_request_params.product.total,
+				currency: wc_stripe_payment_request_params.checkout.currency_code,
+				country: wc_stripe_payment_request_params.checkout.country_code,
+				requestPayerName: true,
+				requestPayerEmail: true,
+				requestPayerPhone: wc_stripe_payment_request_params.checkout.needs_payer_phone,
+				requestShipping: wc_stripe_payment_request_params.product.requestShipping,
+				displayItems: wc_stripe_payment_request_params.product.displayItems
+			};
+		},
+
+		/**
+		 * Starts the payment request
+		 *
+		 * @since 4.0.0
+		 * @version 4.8.0
+		 */
+		startPaymentRequest: function( cart ) {
+			var paymentDetails,
+				options;
+
+			if ( wc_stripe_payment_request_params.is_product_page ) {
+				options = wc_stripe_payment_request.getRequestOptionsFromLocal();
+
+				paymentDetails = options;
+			} else {
+				options = {
+					total: cart.order_data.total,
+					currency: cart.order_data.currency,
+					country: cart.order_data.country_code,
+					requestPayerName: true,
+					requestPayerEmail: true,
+					requestPayerPhone: wc_stripe_payment_request_params.checkout.needs_payer_phone,
+					requestShipping: cart.shipping_required ? true : false,
+					displayItems: cart.order_data.displayItems
+				};
+
+				paymentDetails = cart.order_data;
+			}
+
+			// Puerto Rico (PR) is the only US territory/possession that's supported by Stripe.
+			// Since it's considered a US state by Stripe, we need to do some special mapping.
+			if ( 'PR' === options.country ) {
+				options.country = 'US';
+			}
+
+			// Handle errors thrown by Stripe, so we don't break the product page
+			try {
+				var paymentRequest = stripe.paymentRequest( options );
+
+				var elements = stripe.elements( { locale: wc_stripe_payment_request_params.button.locale } );
+				var prButton = wc_stripe_payment_request.createPaymentRequestButton( elements, paymentRequest );
+
+				// Check the availability of the Payment Request API first.
+				paymentRequest.canMakePayment().then( function( result ) {
+					if ( ! result ) {
+						return;
+					}
+					if ( result.applePay ) {
+						paymentRequestType = 'apple_pay';
+					} else if ( result.googlePay ) {
+						paymentRequestType = 'google_pay';
+					} else {
+						paymentRequestType = 'payment_request_api';
+					}
+
+					wc_stripe_payment_request.attachPaymentRequestButtonEventListeners( prButton, paymentRequest );
+					wc_stripe_payment_request.showPaymentRequestButton( prButton );
+				} );
+
+				// Possible statuses success, fail, invalid_payer_name, invalid_payer_email, invalid_payer_phone, invalid_shipping_address.
+				paymentRequest.on( 'shippingaddresschange', function( evt ) {
+					$.when( wc_stripe_payment_request.updateShippingOptions( paymentDetails, evt.shippingAddress ) ).then( function( response ) {
+						evt.updateWith( { status: response.result, shippingOptions: response.shipping_options, total: response.total, displayItems: response.displayItems } );
+					} );
+				} );
+
+				paymentRequest.on( 'shippingoptionchange', function( evt ) {
+					$.when( wc_stripe_payment_request.updateShippingDetails( paymentDetails, evt.shippingOption ) ).then( function( response ) {
+						if ( 'success' === response.result ) {
+							evt.updateWith( { status: 'success', total: response.total, displayItems: response.displayItems } );
+						}
+
+						if ( 'fail' === response.result ) {
+							evt.updateWith( { status: 'fail' } );
+						}
+					} );
+				} );
+
+				paymentRequest.on( 'source', function( evt ) {
+					// Check if we allow prepaid cards.
+					if ( 'no' === wc_stripe_payment_request_params.stripe.allow_prepaid_card && 'prepaid' === evt.source.card.funding ) {
+						wc_stripe_payment_request.abortPayment( evt, wc_stripe_payment_request.getErrorMessageHTML( wc_stripe_payment_request_params.i18n.no_prepaid_card ) );
+					} else {
+						$.when( wc_stripe_payment_request.processSource( evt, paymentRequestType ) ).then( function( response ) {
+							if ( 'success' === response.result ) {
+								wc_stripe_payment_request.completePayment( evt, response.redirect );
+							} else {
+								wc_stripe_payment_request.abortPayment( evt, response.messages );
+							}
+						} );
+					}
+				} );
+			} catch( e ) {
+				// Leave for troubleshooting
+				console.error( e );
+			}
+		},
+
+		getSelectedProductData: function() {
+			var product_id = $( '.single_add_to_cart_button' ).val();
+
+			// Check if product is a variable product.
+			if ( $( '.single_variation_wrap' ).length ) {
+				product_id = $( '.single_variation_wrap' ).find( 'input[name="product_id"]' ).val();
+			}
+
+			var addons = $( '#product-addons-total' ).data('price_data') || [];
+			var addon_value = addons.reduce( function ( sum, addon ) { return sum + addon.cost; }, 0 );
+
+			var data = {
+				security: wc_stripe_payment_request_params.nonce.get_selected_product_data,
+				product_id: product_id,
+				qty: $( '.quantity .qty' ).val(),
+				attributes: $( '.variations_form' ).length ? wc_stripe_payment_request.getAttributes().data : [],
+				addon_value: addon_value,
+			};
+
+			return $.ajax( {
+				type: 'POST',
+				data: data,
+				url:  wc_stripe_payment_request.getAjaxURL( 'get_selected_product_data' )
+			} );
+		},
+
+		/**
+		 * Creates a wrapper around a function that ensures a function can not
+		 * called in rappid succesion. The function can only be executed once and then agin after
+		 * the wait time has expired.  Even if the wrapper is called multiple times, the wrapped
+		 * function only excecutes once and then blocks until the wait time expires.
+		 *
+		 * @param {int} wait       Milliseconds wait for the next time a function can be executed.
+		 * @param {function} func       The function to be wrapped.
+		 * @param {bool} immediate Overriding the wait time, will force the function to fire everytime.
+		 *
+		 * @return {function} A wrapped function with execution limited by the wait time.
+		 */
+		debounce: function( wait, func, immediate ) {
+			var timeout;
+			return function() {
+				var context = this, args = arguments;
+				var later = function() {
+					timeout = null;
+					if (!immediate) func.apply(context, args);
+				};
+				var callNow = immediate && !timeout;
+				clearTimeout(timeout);
+				timeout = setTimeout(later, wait);
+				if (callNow) func.apply(context, args);
+			};
+		},
+
+		/**
+		 * Creates stripe paymentRequest element or connects to custom button
+		 *
+		 * @param {object} elements       Stripe elements instance.
+		 * @param {object} paymentRequest Stripe paymentRequest object.
+		 *
+		 * @return {object} Stripe paymentRequest element or custom button jQuery element.
+		 */
+		createPaymentRequestButton: function( elements, paymentRequest ) {
+			var button;
+			if ( wc_stripe_payment_request_params.button.is_custom ) {
+				button = $( wc_stripe_payment_request_params.button.css_selector );
+				if ( button.length ) {
+					// We fallback to default paymentRequest button if no custom button is found in the UI.
+					// Add flag to be sure that created button is custom button rather than fallback element.
+					button.data( 'isCustom', true );
+					return button;
+				}
+			}
+
+			if ( wc_stripe_payment_request_params.button.is_branded ) {
+				if ( wc_stripe_payment_request.shouldUseGooglePayBrand() ) {
+					button = wc_stripe_payment_request.createGooglePayButton();
+					// Add flag to be sure that created button is branded rather than fallback element.
+					button.data( 'isBranded', true );
+					return button;
+				} else {
+					// Not implemented branded buttons default to Stripe's button
+					// Apple Pay buttons can also fall back to Stripe's button, as it's already branded
+					// Set button type to default or buy, depending on branded type, to avoid issues with Stripe
+					wc_stripe_payment_request_params.button.type = 'long' === wc_stripe_payment_request_params.button.branded_type ? 'buy' : 'default';
+				}
+			}
+
+			return elements.create( 'paymentRequestButton', {
+				paymentRequest: paymentRequest,
+				style: {
+					paymentRequestButton: {
+						type: wc_stripe_payment_request_params.button.type,
+						theme: wc_stripe_payment_request_params.button.theme,
+						height: wc_stripe_payment_request_params.button.height + 'px',
+					},
+				},
+			} );
+		},
+
+		/**
+		 * Checks if button is custom payment request button.
+		 *
+		 * @param {object} prButton Stripe paymentRequest element or custom jQuery element.
+		 *
+		 * @return {boolean} True when prButton is custom button jQuery element.
+		 */
+		isCustomPaymentRequestButton: function ( prButton ) {
+			return prButton && 'function' === typeof prButton.data && prButton.data( 'isCustom' );
+		},
+
+		isBrandedPaymentRequestButton: function ( prButton ) {
+			return prButton && 'function' === typeof prButton.data && prButton.data( 'isBranded' );
+		},
+
+		shouldUseGooglePayBrand: function () {
+			var ua = window.navigator.userAgent.toLowerCase();
+			var isChrome = /chrome/.test( ua ) && ! /edge|edg|opr|brave\//.test( ua ) && 'Google Inc.' === window.navigator.vendor;
+			// newer versions of Brave do not have the userAgent string
+			var isBrave = isChrome && window.navigator.brave;
+			return isChrome && ! isBrave;
+		},
+
+		createGooglePayButton: function () {
+			var allowedThemes = [ 'dark', 'light', 'light-outline' ];
+			var allowedTypes = [ 'short', 'long' ];
+
+			var theme  = wc_stripe_payment_request_params.button.theme;
+			var type   = wc_stripe_payment_request_params.button.branded_type;
+			var locale = wc_stripe_payment_request_params.button.locale;
+			var height = wc_stripe_payment_request_params.button.height;
+			theme = allowedThemes.includes( theme ) ? theme : 'light';
+			var gpaySvgTheme = 'dark' === theme ? 'dark' : 'light';
+			type = allowedTypes.includes( type ) ? type : 'long';
+
+			var button = $( '<button type="button" id="wc-stripe-branded-button" aria-label="Google Pay" class="gpay-button"></button>' );
+			button.css( 'height', height + 'px' );
+			button.addClass( theme + ' ' + type );
+			if ( 'long' === type ) {
+				var url = 'https://www.gstatic.com/instantbuy/svg/' + gpaySvgTheme + '/' + locale + '.svg';
+				var fallbackUrl = 'https://www.gstatic.com/instantbuy/svg/' + gpaySvgTheme + '/en.svg';
+				// Check if locale GPay button exists, default to en if not
+				setBackgroundImageWithFallback( button, url, fallbackUrl );
+			}
+
+			return button;
+		},
+
+		attachPaymentRequestButtonEventListeners: function( prButton, paymentRequest ) {
+			// First, mark the body so we know a payment request button was used.
+			// This way error handling can any display errors in the most appropriate place.
+			prButton.on( 'click', function ( evt ) {
+				$( 'body' ).addClass( 'woocommerce-stripe-prb-clicked' );
+			});
+
+			// Then, attach specific handling for selected pages and button types
+			if ( wc_stripe_payment_request_params.is_product_page ) {
+				wc_stripe_payment_request.attachProductPageEventListeners( prButton, paymentRequest );
+			} else {
+				wc_stripe_payment_request.attachCartPageEventListeners( prButton, paymentRequest );
+			}
+		},
+
+		attachProductPageEventListeners: function( prButton, paymentRequest ) {
+			var paymentRequestError = [];
+			var addToCartButton = $( '.single_add_to_cart_button' );
+
+			prButton.on( 'click', function ( evt ) {
+				// If login is required for checkout, display redirect confirmation dialog.
+				if ( wc_stripe_payment_request_params.login_confirmation ) {
+					evt.preventDefault();
+					displayLoginConfirmation( paymentRequestType );
+					return;
+				}
+
+				// First check if product can be added to cart.
+				if ( addToCartButton.is( '.disabled' ) ) {
+					evt.preventDefault(); // Prevent showing payment request modal.
+					if ( addToCartButton.is( '.wc-variation-is-unavailable' ) ) {
+						window.alert( wc_add_to_cart_variation_params.i18n_unavailable_text );
+					} else if ( addToCartButton.is( '.wc-variation-selection-needed' ) ) {
+						window.alert( wc_add_to_cart_variation_params.i18n_make_a_selection_text );
+					}
+					return;
+				}
+
+				if ( 0 < paymentRequestError.length ) {
+					evt.preventDefault();
+					window.alert( paymentRequestError );
+					return;
+				}
+
+				wc_stripe_payment_request.addToCart();
+
+				if ( wc_stripe_payment_request.isCustomPaymentRequestButton( prButton ) || wc_stripe_payment_request.isBrandedPaymentRequestButton( prButton ) ) {
+					evt.preventDefault();
+					paymentRequest.show();
+				}
+			});
+
+			$( document.body ).on( 'wc_stripe_unblock_payment_request_button wc_stripe_enable_payment_request_button', function () {
+				wc_stripe_payment_request.unblockPaymentRequestButton();
+			} );
+
+			$( document.body ).on( 'wc_stripe_block_payment_request_button', function () {
+				wc_stripe_payment_request.blockPaymentRequestButton( 'wc_request_button_is_blocked' );
+			} );
+
+			$( document.body ).on( 'wc_stripe_disable_payment_request_button', function () {
+				wc_stripe_payment_request.blockPaymentRequestButton( 'wc_request_button_is_disabled' );
+			} );
+
+			$( document.body ).on( 'woocommerce_variation_has_changed', function () {
+				$( document.body ).trigger( 'wc_stripe_block_payment_request_button' );
+
+				$.when( wc_stripe_payment_request.getSelectedProductData() ).then( function ( response ) {
+					$.when(
+						paymentRequest.update( {
+							total: response.total,
+							displayItems: response.displayItems,
+						} )
+					).then( function () {
+						$( document.body ).trigger( 'wc_stripe_unblock_payment_request_button' );
+					} );
+				});
+			} );
+
+			// Block the payment request button as soon as an "input" event is fired, to avoid sync issues
+			// when the customer clicks on the button before the debounced event is processed.
+			$( '.quantity' ).on( 'input', '.qty', function() {
+				$( document.body ).trigger( 'wc_stripe_block_payment_request_button' );
+			} );
+
+			$( '.quantity' ).on( 'input', '.qty', wc_stripe_payment_request.debounce( 250, function() {
+				$( document.body ).trigger( 'wc_stripe_block_payment_request_button' );
+				paymentRequestError = [];
+
+				$.when( wc_stripe_payment_request.getSelectedProductData() ).then( function ( response ) {
+					if ( response.error ) {
+						paymentRequestError = [ response.error ];
+						$( document.body ).trigger( 'wc_stripe_unblock_payment_request_button' );
+					} else {
+						$.when(
+							paymentRequest.update( {
+								total: response.total,
+								displayItems: response.displayItems,
+							} )
+						).then( function () {
+							$( document.body ).trigger( 'wc_stripe_unblock_payment_request_button' );
+						});
+					}
+				} );
+			}));
+
+			if ( $('.variations_form').length ) {
+				$( '.variations_form' ).on( 'found_variation.wc-variation-form', function ( evt, variation ) {
+					if ( variation.is_in_stock ) {
+						wc_stripe_payment_request.unhidePaymentRequestButton();
+					} else {
+						wc_stripe_payment_request.hidePaymentRequestButton();
+					}
+				} );
+			}
+		},
+
+		attachCartPageEventListeners: function ( prButton, paymentRequest ) {
+			prButton.on( 'click', function ( evt ) {
+				// If login is required for checkout, display redirect confirmation dialog.
+				if ( wc_stripe_payment_request_params.login_confirmation ) {
+					evt.preventDefault();
+					displayLoginConfirmation( paymentRequestType );
+					return;
+				}
+
+				if (
+					wc_stripe_payment_request.isCustomPaymentRequestButton(
+						prButton
+					) ||
+					wc_stripe_payment_request.isBrandedPaymentRequestButton(
+						prButton
+					)
+				) {
+					evt.preventDefault();
+					paymentRequest.show();
+				}
+			} );
+		},
+
+		showPaymentRequestButton: function( prButton ) {
+			if ( wc_stripe_payment_request.isCustomPaymentRequestButton( prButton ) ) {
+				prButton.addClass( 'is-active' );
+				$( '#wc-stripe-payment-request-wrapper, #wc-stripe-payment-request-button-separator' ).show();
+			} else if ( wc_stripe_payment_request.isBrandedPaymentRequestButton( prButton ) ) {
+				$( '#wc-stripe-payment-request-wrapper, #wc-stripe-payment-request-button-separator' ).show();
+				$( '#wc-stripe-payment-request-button' ).html( prButton );
+			} else if ( $( '#wc-stripe-payment-request-button' ).length ) {
+				$( '#wc-stripe-payment-request-wrapper, #wc-stripe-payment-request-button-separator' ).show();
+				prButton.mount( '#wc-stripe-payment-request-button' );
+			}
+		},
+
+		hidePaymentRequestButton: function () {
+			$( '#wc-stripe-payment-request-wrapper, #wc-stripe-payment-request-button-separator' ).hide();
+		},
+
+		unhidePaymentRequestButton: function () {
+			const stripe_wrapper = $( '#wc-stripe-payment-request-wrapper' );
+			const stripe_separator = $( '#wc-stripe-payment-request-button-separator' );
+			// If either element is hidden, ensure both show.
+			if ( stripe_wrapper.is(':hidden') || stripe_separator.is(':hidden') ) {
+				stripe_wrapper.show();
+				stripe_separator.show();
+			}
+		},
+
+		blockPaymentRequestButton: function( cssClassname ) {
+			// check if element isn't already blocked before calling block() to avoid blinking overlay issues
+			// blockUI.isBlocked is either undefined or 0 when element is not blocked
+			if ( $( '#wc-stripe-payment-request-button' ).data( 'blockUI.isBlocked' ) ) {
+				return;
+			}
+
+			$( '#wc-stripe-payment-request-button' )
+				.addClass( cssClassname )
+				.block( { message: null } );
+		},
+
+		unblockPaymentRequestButton: function() {
+			$( '#wc-stripe-payment-request-button' )
+				.removeClass( ['wc_request_button_is_blocked', 'wc_request_button_is_disabled'] )
+				.unblock();
+		},
+
+		/**
+		 * Initialize event handlers and UI state
+		 *
+		 * @since 4.0.0
+		 * @version 4.0.0
+		 */
+		init: function() {
+			if ( wc_stripe_payment_request_params.is_product_page ) {
+				wc_stripe_payment_request.startPaymentRequest( '' );
+			} else {
+				wc_stripe_payment_request.getCartDetails();
+			}
+
+		},
+	};
+
+	wc_stripe_payment_request.init();
+
+	// We need to refresh payment request data when total is updated.
+	$( document.body ).on( 'updated_cart_totals', function() {
+		wc_stripe_payment_request.init();
+	} );
+
+	// We need to refresh payment request data when total is updated.
+	$( document.body ).on( 'updated_checkout', function() {
+		wc_stripe_payment_request.init();
+	} );
+
+	function setBackgroundImageWithFallback( element, background, fallback ) {
+		element.css( 'background-image', 'url(' + background + ')' );
+		// Need to use an img element to avoid CORS issues
+		var testImg = document.createElement("img");
+		testImg.onerror = function () {
+			element.css( 'background-image', 'url(' + fallback + ')' );
+		}
+		testImg.src = background;
+	}
+
+	// TODO: Replace this by `client/blocks/payment-request/login-confirmation.js`
+	// when we start using webpack to build this file.
+	function displayLoginConfirmation( paymentRequestType ) {
+		if ( ! wc_stripe_payment_request_params.login_confirmation ) {
+			return;
+		}
+
+		var message = wc_stripe_payment_request_params.login_confirmation.message;
+
+		// Replace dialog text with specific payment request type "Apple Pay" or "Google Pay".
+		if ( 'payment_request_api' !== paymentRequestType ) {
+			message = message.replace(
+				/\*\*.*?\*\*/,
+				'apple_pay' === paymentRequestType ? 'Apple Pay' : 'Google Pay'
+			);
+		}
+
+		// Remove asterisks from string.
+		message = message.replace( /\*\*/g, '' );
+
+		if ( confirm( message ) ) {
+			// Redirect to my account page.
+			window.location.href = wc_stripe_payment_request_params.login_confirmation.redirect_url;
+		}
+	}
+} );