PageSourceSearch

https://varsitydesmoines.com/content/ux/boxoffice/js/checkout.js?ver=e91c1fa2

js varsitydesmoines.com collected 2026-10-02 10:06:31 UTC 122,274 bytes, 3,400 lines download raw bytes

1/* jshint camelcase: false */
2/* globals themeCheckout, STRIPE_PUBLIC_KEY, STRIPE_CONNECTED_ACCOUNT_ID, BOOKING_FEE, Sentry */
3'use strict';
4
5(function ( $ ) {
6	const filters = {};
7
8	$.addFilter = function ( name, callback ) {
9		if ( !filters[name] ) {
10			filters[name] = [];
11		}
12		filters[name].push( callback );
13	};
14
15	$.applyFilter = function ( name, data ) {
16		if ( filters[name] ) {
17			return filters[name].reduce( function ( data, callback ) {
18				return callback( data );
19			}, data );
20		} else {
21			return data;
22		}
23	};
24})( jQuery );
25
26(function ( $, Stripe ) {
27
28	window.themeCheckout = {
29		init: function () {
30			themeCheckout.initProps();
31			themeCheckout.attachEvents();
32		},
33		initProps: function () {
34			themeCheckout.$quantitySelect    = $( '.ticket-quantity select' );
35			themeCheckout.$checkout          = $( '#checkout' );
36			themeCheckout.$checkoutContainer = themeCheckout.$checkout.find( '.checkout-container' );
37			themeCheckout.$form              = $( '#payment-details' );
38			themeCheckout.order              = {
39				showtime: $( '#sid' ).val(),
40				tickets: {},
41				total: 0,
42				bookingFee: 0,
43				discount: 0,
44				donation: 0
45			};
46
47			// checkout form
48			themeCheckout.paymentMethod      = 'cc'; // Payment method can be 'cc', 'gc', 'gc_partial', 'cash'
49			themeCheckout.ccMethod           = 'manual-cc'; // Payment method related to credit cards can be 'manual-cc', 'apple-pay', 'google-pay'
50			themeCheckout.$fields            = $( '.payment-address' ).find( 'input[type=text], input[type=email]' ).not( '#discount-code' );
51			themeCheckout.$ccFields          = $( '.payment-info' );
52			themeCheckout.$emailInput        = $( '#email' );
53			themeCheckout.$gcInput           = $( '#gc' );
54			themeCheckout.$gcCvvInput        = $( '#gc-cvv' );
55			themeCheckout.$terms             = $( '.terms' );
56			themeCheckout.$submitButton      = $( '#submit-purchase' );
57			themeCheckout.$submitError       = $( '.submit-error' );
58			themeCheckout.$orderConfirmation = $( '#order-confirmation' );
59			themeCheckout.$inactiveForm      = $( '.checkout-container' ).not( '.checkout-active' ).find( themeCheckout.$form );
60			themeCheckout.$donationAmount    = $( '.donation-form input' );
61			themeCheckout.$donateAmount      = $( '#donate-me' );
62			themeCheckout.$donationVariants  = $( '.donation-variant' );
63			themeCheckout.$customDonationAmt = $( '.custom-donation-amount' );
64			themeCheckout.$pmSelector        = $( '.payment-method-type-selector' );
65			themeCheckout.$pmSelectorItem    = $( '.payment-method-option' );
66			themeCheckout.$pmMethodSelect    = $( '#payment-method-select' );
67			themeCheckout.hasAdditionalPMs   = false;
68			themeCheckout.cardPaymentMethod  = null;
69
70			themeCheckout.stripePaymentRequestEvent = null;
71
72			// Gift Cards.
73			themeCheckout.$giftCardWrap  = $( document.getElementById( 'gc-wrap' ) );
74			themeCheckout.$giftCard      = $( document.getElementById( 'gc' ) );
75			themeCheckout.$giftCardCvv   = $( document.getElementById( 'gc-cvv' ) );
76			themeCheckout.$giftCardApply = $( document.getElementById( 'apply-gc' ) );
77			themeCheckout.$giftCardError = $( document.getElementById( 'gc-error-message' ) );
78			themeCheckout.$giftCardTotal = $( '.gift-card' );
79			themeCheckout.$giftCardRow   = $( '.gift-card-row' );
80
81			// discount codes
82			themeCheckout.$discountCode        = $( '#discount-code' );
83			themeCheckout.$discountCodeApply   = $( '#apply-discount-code' );
84			themeCheckout.$discountCodeError   = $( '#discount-code-error-message' );
85			themeCheckout.$discountCodeSuccess = $( '#discount-code-success-message' );
86			themeCheckout.$discount            = $( '.discount' );
87
88			// memberships
89			themeCheckout.$membershipSignup   = $( '.membership-signup' );
90			themeCheckout.$membershipPlanSize = $( '#membership-plan-size-toggle [type="radio"]' );
91			themeCheckout.$membershipPlanId   = $( '#membership_plan_current_id' );
92			themeCheckout.$membershipsInfo    = $( '.membership_type_value' ).data( 'json' );
93			if ( themeUx.$body.is( '.membership-renew' ) ) {
94				themeCheckout.setMembershipRenewalDetails();
95			}
96
97			// ticket packs
98			themeCheckout.$ticketPackPurchase = $( '.ticket-pack-purchase' );
99			themeCheckout.$ticketPackValue    = $( '.ticket-pack-value' ).data( 'json' );
100
101			// dnd
102			themeCheckout.$dnd         = $( '#dnd' );
103			themeCheckout.$dndCheckbox = themeCheckout.$dnd.find( 'input[type=checkbox]' );
104			themeCheckout.$dndStep2    = $( '#dnd-step-2' );
105			themeCheckout.$phone       = $( '#phone' );
106			themeCheckout.$phone.prop( 'disabled', true );
107			themeCheckout.$tipOptions = $( '#tip-options' );
108			themeCheckout.$tipPercent = $( '#tip-percent' );
109
110			// reserved seating
111			themeCheckout.$overlayMessage     = $( '.overlay-message' );
112			themeCheckout.$reservedOrderId    = null;
113			themeCheckout.$seatsData          = {};
114			themeCheckout.$selectedSeats      = [];
115			themeCheckout.$reservedSeats      = [];
116			themeCheckout.$hall               = null;
117			themeCheckout.$inRequest          = false;
118			themeCheckout.$seatsModal         = $( '.seats-modal' );
119			themeCheckout.$checkoutSeatsModal = $( '.seats-modal-checkout' );
120			themeCheckout.$cancelSeatsModal   = $( '.seats-modal-cancel' );
121			themeCheckout.$openSeatsModal     = $( '.open-seats-modal' );
122			themeCheckout.$rsShowtimePicker   = $( '.rs-select-showtime-container' );
123			themeCheckout.$rsShowtimeSelect   = $( '.rs-select-showtime' );
124			themeCheckout.$rsSelectedShowtime = $( '.rs-selected-showtime' );
125			themeCheckout.seatPropertiesCache = {};
126			themeCheckout.refreshableProps();
127			themeCheckout.seatsADAList       = $( '#selector-seats' );
128			themeCheckout.$sidebar           = $( '#sidebar' );
129			themeCheckout.$siteHeader        = $( '#site-header, #mobile-nav' );
130			themeCheckout.$siteFooter        = $( '#footer-container, #footer-nav' );
131			themeCheckout.seatsADAListOption = $( '#selector-seats option' );
132			themeCheckout.ADAModeOn          = $( '.accessibility-mode-on' );
133			themeCheckout.ADAModeOff         = $( '.accessibility-mode-off' );
134			themeCheckout.$shortSeatsList    = [];
135
136			themeCheckout.ADAContainer       = $( '.ada-container' );
137			themeCheckout.ADAMode            = false;
138			themeCheckout.seatmapContainer   = $( '.hall-container' );
139			themeCheckout.lastValidSelection = null;
140			themeCheckout.selectedADASeat    = [];
141
142			// check if there are membership discount codes available
143			themeCheckout.$getCodeLookupNonce    = $( '#fb_membership_lookup_code_nonce' );
144			themeCheckout.$discountCodeRow       = $( '.discount-code-row' );
145			themeCheckout.$discountCodeField     = $( '#discount-code' );
146			themeCheckout.requestingDiscountCode        = false;
147			themeCheckout.applyingDiscountCode          = false;
148			themeCheckout.appliedCodeForEmail           = '';
149			themeCheckout.membershipCodeRemovedForEmail = '';
150
151			// themeCheckout.$inactiveForm = $( '.checkout-container' ).not( '.checkout-active' ).find( themeCheckout.$form );
152			themeCheckout.$isMobile = window.matchMedia( 'only screen and (max-width: 667px)' );
153			themeCheckout.$isTablet = window.matchMedia( 'only screen and (max-width: 1024px)' );
154
155			// prevent double submits
156			themeCheckout.checkoutSubmitted = false;
157
158			// prevent Payment Method selection if reservation has expired
159			themeCheckout.hasReservationExpired = false;
160
161			// Flag for whether Stripe Elements has been initialized.
162			themeCheckout.stripeElementsInit = false;
163
164			themeCheckout.currentDonationAmount        = 0;
165			themeCheckout.maxUnconfirmedDonationAmount = 50;
166		},
167		refreshableProps: function () {
168			themeCheckout.$seatTicketTypeSelect = $( '.seat-ticket-type .select-ticket-type' ).not( '.hidden' ).find( 'select' );
169		},
170		moneyFormat: function ( value ) {
171			var locale = window.themeSettings.locale ? window.themeSettings.locale : 'en-US';
172			locale     = locale.replace( '_', '-' );
173			return parseFloat( value ).toLocaleString( locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
174		},
175		setDonationAmount: function ( amount ) {
176			amount                              = themeCheckout.roundDecimalValue( parseFloat( amount ) );
177			themeCheckout.currentDonationAmount = amount;
178			var $donationRow                    = $( '.donate' );
179			var isDonationVisible               = $donationRow.is( ':visible' );
180
181			if ( (amount > 0 && !isDonationVisible) || (!amount && isDonationVisible) ) {
182				$donationRow.slideToggle( '200' );
183			}
184
185			$( '.donate-amount' ).text( themeCheckout.moneyFormat( amount ) );
186
187			if ( window.themeUx.$body.hasClass( 'membership-payment' ) || window.themeUx.$body.hasClass( 'membership-renew' ) ) {
188				themeCheckout.order.donation = amount;
189				themeCheckout.updateOrderTotal( themeCheckout.order.membership + themeCheckout.order.donation );
190				$( '.total_purchase_price' ).text( themeCheckout.moneyFormat( themeCheckout.order.total ) );
191				themeCheckout.maybeToggleCreditCardFields();
192				return;
193			}
194
195			if ( $( '.seat-ticket-type' ).length > 0 ) {
196				themeCheckout.updateQuantityBySeats();
197			} else {
198				themeCheckout.updateQuantity();
199			}
200
201			themeCheckout.maybeReapplyGiftCard();
202		},
203		getCurrentMembershipPlanId: function () {
204			var membershipPlanIdField = themeCheckout.$membershipPlanId;
205			return membershipPlanIdField.length ? themeCheckout.$membershipPlanId.val() : null;
206		},
207		getCurrentMembershipPlanInfo: function () {
208			if ( !themeCheckout.$membershipsInfo ) {
209				return {};
210			}
211			var currentPlanId      = themeCheckout.getCurrentMembershipPlanId();
212			var membershipPlanInfo = themeCheckout.$membershipsInfo[currentPlanId];
213			return membershipPlanInfo ? membershipPlanInfo : {};
214		},
215		getCurrentMembershipPlanCosts: function () {
216			var membershipPlanInfo = themeCheckout.getCurrentMembershipPlanInfo();
217			return membershipPlanInfo['costs'] ? membershipPlanInfo['costs'] : {};
218		},
219		updateOrderMembershipSize: function ( planType ) {
220			var currentPlanInfo = themeCheckout.getCurrentMembershipPlanInfo();
221
222			themeCheckout.order.membership            = parseFloat( currentPlanInfo['costs'][planType] );
223			themeCheckout.order.membership_plan.id    = currentPlanInfo['id'];
224			themeCheckout.order.membership_plan.title = currentPlanInfo['title'];
225			themeCheckout.order.membership_plan.size  = planType;
226			themeCheckout.updateOrderTotal( themeCheckout.order.membership + themeCheckout.order.donation );
227
228			$( '.total_purchase_price' ).text( themeCheckout.moneyFormat( themeCheckout.order.total ) );
229			themeCheckout.maybeToggleCreditCardFields();
230		},
231		attachEvents: function () {
232			// update page on select ticket quantity
233			themeCheckout.$quantitySelect.change( function () {
234				themeCheckout.toggleActiveCheckout();
235				themeCheckout.manageQtysAvailable();
236				themeCheckout.applyDiscountCode();
237			} );
238
239			// Initialize hosted credit card form and payment request if Stripe is enabled.
240			if ( typeof STRIPE_PUBLIC_KEY !== 'undefined' && !!STRIPE_PUBLIC_KEY ) {
241
242				// Initialize Stripe Elements for displaying the credit card form.
243				themeCheckout.initStripeElements();
244
245				// Checks if Stripe Elements are initialized.
246				themeCheckout.initStripeElementsChecks();
247
248				// Initialize payment method request and checks if payment request is supported.
249				themeCheckout.initPaymentRequest();
250			}
251
252			// validate checkout form on submit
253			themeCheckout.$form.submit( async function ( e ) {
254				themeCheckout.disableForm();
255				var isValidForm = await themeCheckout.validateForm( e );
256				if ( !isValidForm ) {
257					themeCheckout.enableForm( 'Please fix the errors before continuing.' );
258
259					themeCheckout.validateStandaloneDonationAmount();
260				}
261			} );
262
263			// validate terms checkbox
264			if ( themeCheckout.$terms.length ) {
265				$( 'input[name=terms]' ).click( function () {
266					themeCheckout.validateTermsCheckbox();
267				} );
268			}
269
270			// validate gc number (not empty)
271			themeCheckout.$gcInput.blur( function () {
272				if ( themeCheckout.paymentMethod === 'gc' || themeCheckout.paymentMethod === 'gc_partial' ) {
273					themeCheckout.validateInputs( $( this ) );
274				}
275			} );
276
277			// validate gc cvv (not empty)
278			themeCheckout.$gcCvvInput.blur( function () {
279				if ( themeCheckout.paymentMethod === 'gc' || themeCheckout.paymentMethod === 'gc_partial' ) {
280					themeCheckout.validateInputs( $( this ) );
281				}
282			} );
283
284			// validate text fields (not empty)
285			themeCheckout.$fields.blur( function () {
286				themeCheckout.validateInputs( $( this ) );
287			} );
288
289			// validate email fields (valid email)
290			themeCheckout.$emailInput.blur( function () {
291				themeCheckout.checkEmailMaybeApplyMembershipCode();
292			} );
293
294			// show zero quantity selected error
295			themeCheckout.$inactiveForm.click( function () {
296				if ( !$( '.checkout-active' ).length ) {
297					$( '.ticket-quantity' ).addClass( 'no-quantity' );
298					themeCheckout.scrollUp();
299				}
300			} );
301
302			// validate donation amount
303			themeCheckout.$donationAmount.on( 'change', function () {
304				var $this             = $( this );
305				var rawDonationAmount = $this.val();
306				rawDonationAmount     = !rawDonationAmount || isNaN( rawDonationAmount ) ? 0 : themeCheckout.roundDecimalValue( parseFloat( rawDonationAmount ) );
307				var donationAmount    = rawDonationAmount.toFixed( 2 );
308
309				// Set the donation amount in the UI and on the themeCheckout.order object.
310				$this.val( donationAmount );
311				$( '.total_purchase_price' ).text( donationAmount );
312
313				themeCheckout.order.donation = donationAmount;
314				themeCheckout.updateOrderTotal( donationAmount );
315
316				themeCheckout.maybeReapplyGiftCard();
317			} );
318
319			// include donation in checkout (checkbox)
320			themeCheckout.$donateAmount.on( 'click', function () {
321				var isChecked      = themeCheckout.$donateAmount.is( ':checked' );
322				var donationAmount = parseFloat( themeCheckout.$donateAmount.val() );
323				// If checkbox is checked, set the donation amount to the value of the donation input. Otherwise, set it to 0.
324				themeCheckout.setDonationAmount( isChecked ? donationAmount : 0 );
325			} );
326
327			// include donation in checkout (freeform)
328			themeCheckout.$donationVariants.on( 'click', function ( event ) {
329
330				// Do not allow to change donation amount if checkout is already submitted
331				if ( themeCheckout.checkoutSubmitted ) {
332					return;
333				}
334
335				var $this             = $( event.target );
336				var isAlreadySelected = $this.hasClass( 'selected' );
337				var isCustomVariant   = $this.hasClass( 'custom-donation-variant' );
338				var isTextInput       = $this.hasClass( 'custom-donation-amount' );
339				var donationAmount    = parseFloat( $this.data( 'donation-amount' ) );
340
341				// We can click on input and this can be counted as click on parent element, so we need to check if it's input
342				if ( isTextInput || isCustomVariant ) {
343					var $customVariantInput   = isTextInput ? $this : $this.find( '.custom-donation-amount' );
344					var $customVariantWrapper = isTextInput ? $this.parent() : $this;
345					// Set the donation amount to the value of the donation input.
346					donationAmount            = $customVariantInput.val() ? parseFloat( $customVariantInput.val() ) : 0;
347					themeCheckout.$donationVariants.removeClass( 'selected' );
348					// We don't need to remove class 'selected', as custom variant can be clicked many times to change amount
349					$customVariantWrapper.addClass( 'selected' );
350					themeCheckout.setDonationAmount( donationAmount );
351					$customVariantInput.focus();
352				} else {
353					themeCheckout.$donationVariants.removeClass( 'selected' );
354					// If standard variant is clicked, and it's already selected, we need to deselect it...
355					isAlreadySelected ? $this.removeClass( 'selected' ) : $this.addClass( 'selected' );
356					// ...and set the donation amount to 0 instead of the value of the donation variant.
357					themeCheckout.setDonationAmount( isAlreadySelected ? 0 : donationAmount );
358				}
359			} );
360
361			themeCheckout.$customDonationAmt.on( 'input', function () {
362
363				// Do not allow to change donation amount if checkout is already submitted
364				if ( themeCheckout.checkoutSubmitted ) {
365					return;
366				}
367
368				var donationAmount = themeCheckout.$customDonationAmt.val().replace( /[^0-9.]/g, '' );
369
370				// Allow clearing the field
371				if ( donationAmount === '' ) {
372					themeCheckout.$customDonationAmt.val( '' );
373					themeCheckout.setDonationAmount( 0 );
374					return;
375				}
376
377				// Check how many . are in the input. If last symbol is . but we already have one, remove all other dots
378				var dots = donationAmount.match( /\./g );
379				if ( dots && dots.length > 1 && donationAmount.charAt( donationAmount.length - 1 ) === '.' ) {
380					donationAmount = donationAmount.substring( 0, donationAmount.length - 1 );
381				}
382				// Disallow values less than 1
383				if ( donationAmount.charAt( 0 ) === '0' || parseFloat( donationAmount ) < 1 ) {
384					donationAmount = '1';
385				}
386				// if we have dot, cut 6 symbols instead of 5
387				donationAmount = (dots && dots.length > 0) ? donationAmount.substring( 0, 6 ) : donationAmount.substring( 0, 5 );
388				// if we have more than 2 symbols after dot, cut it to 2 symbols after dot
389				if ( dots && dots.length > 0 && donationAmount.indexOf( '.' ) !== -1 ) {
390					var parts = donationAmount.split( '.' );
391					if ( parts[1].length > 2 ) {
392						donationAmount = parts[0] + '.' + parts[1].substring( 0, 2 );
393					}
394				}
395				donationAmount = donationAmount && donationAmount !== '.' ? donationAmount : '1';
396				themeCheckout.$customDonationAmt.val( donationAmount );
397				themeCheckout.setDonationAmount( donationAmount );
398			} );
399
400			// update membership plan cost
401			themeCheckout.$membershipPlanSize.on( 'change', function () {
402				var $this    = $( this ),
403					planType = $this.val();
404
405				themeCheckout.updateOrderMembershipSize( planType );
406			} );
407
408			// Apply gift card.
409			themeCheckout.$giftCardApply.on( 'click', function ( event ) {
410				event.preventDefault();
411
412				themeCheckout.$giftCardError.html( '' ).css( 'visibility', 'hidden' );
413
414				if ( true === themeCheckout.$giftCard.prop( 'readonly' ) && 'REMOVE' === themeCheckout.$giftCardApply.html() ) {
415					themeCheckout.removeGiftCard();
416					return false;
417				}
418
419				themeCheckout.applyGiftCard();
420			} );
421
422			// apply discount code
423			themeCheckout.$discountCodeApply.on( 'click', function ( e ) {
424				e.preventDefault();
425				themeCheckout.startApplyingDiscountCode();
426			} );
427
428			// dnd
429			themeCheckout.$dndCheckbox.on( 'click', function ( e ) {
430				if ( $( this ).is( ':checked' ) ) {
431					themeCheckout.$dndStep2.show();
432					// order ahead / phone
433					// themeCheckout.$phone.prop( 'disabled', false );
434					// themeCheckout.$phone.on( 'blur', function () {
435					// 	themeCheckout.validatePhoneNumber( $( this ) );
436					// } );
437
438					// pre-set tip
439					themeCheckout.$tipPercent.prop( 'disabled', false );
440					themeCheckout.$tipPercent.on( 'blur', function () {
441						themeCheckout.validateTipPercent( $( this ) );
442					} );
443					themeCheckout.$fields = themeCheckout.$fields.add( themeCheckout.$tipPercent );
444					if ( themeCheckout.$tipPercent.val() === "" ) {
445						themeCheckout.$tipOptions.find( "li:nth-child(2) a" ).trigger( 'click' ).focus();
446					}
447				} else {
448					themeCheckout.$dndStep2.hide();
449
450					// order ahead / phone
451					// themeCheckout.$phone.prop( 'disabled', true );
452					// themeCheckout.$phone.off( 'blur' );
453					// themeCheckout.$phone.removeClass( 'error' );
454
455					// pre-set tip
456					themeCheckout.$tipPercent.prop( 'disabled', true );
457					themeCheckout.$tipPercent.off( 'blur' );
458					themeCheckout.$tipPercent.removeClass( 'error' );
459					themeCheckout.$fields = themeCheckout.$fields.not( themeCheckout.$tipPercent );
460				}
461			} );
462
463			$( '.dnd-explainer' ).on( 'click', function ( e ) {
464				e.preventDefault();
465				var $modal = $( '.dnd-explainer-modal' );
466				$modal.addClass( 'active' );
467			} );
468
469			$( '.dnd-explainer-modal .modal-backdrop, .dnd-explainer-modal .close-modal' ).on( 'click', function () {
470				$( '.modal.active' ).removeClass( 'active' );
471			} );
472
473			themeCheckout.$tipOptions.find( 'a' ).on( 'click', function ( e ) {
474				e.preventDefault();
475				var percent = $( this ).data( 'tip_percent' );
476				themeCheckout.$tipPercent.val( percent );
477			} );
478
479			// This event handles switching between payment methods in checkout
480			themeCheckout.$pmSelectorItem.on( 'click', function ( e ) {
481				e.preventDefault();
482
483				// Do not allow to change payment method if checkout is already submitted
484				if ( themeCheckout.checkoutSubmitted ) {
485					return;
486				}
487
488				// Do not allow to switch to other payment methods if checkout is not active
489				if ( !themeCheckout.$checkoutContainer.hasClass( 'checkout-active' ) ) {
490					return;
491				}
492				themeCheckout.selectPaymentMethodType( $( this ) );
493			} );
494
495			// reserved seating
496
497			// fetch seats data from auditorium
498			if ( themeCheckout.order.showtime ) {
499				themeCheckout.getSeatsData();
500			}
501
502			// handle Checkout from Seats Modal
503			themeCheckout.$checkoutSeatsModal.click( function () {
504				themeCheckout.clearErrors();
505				themeCheckout.reserveSeats();
506			} );
507
508			// handle Cancel from Seats Modal
509			themeCheckout.$cancelSeatsModal.on( 'click', themeCheckout.hideSeatsModal );
510			themeCheckout.$openSeatsModal.on( 'click', themeCheckout.getSeatsData );
511
512			// handle Change seat ticket type
513			themeCheckout.$seatTicketTypeSelect.change( themeCheckout.changeReservedSeatTicketType );
514
515			// handle change showtimes
516			themeCheckout.$rsShowtimePicker.find( 'select' ).on( 'change', function () {
517				window.location = $( this ).val();
518			} );
519
520			// show ADA
521			themeCheckout.ADAModeOn.click( function () {
522				themeCheckout.ADAContainer.show();
523				themeCheckout.seatmapContainer.hide();
524				themeCheckout.ADAMode = true;
525				themeCheckout.ADAModeOn.hide();
526				themeCheckout.$siteHeader.hide();
527				themeCheckout.$siteFooter.css( 'display', 'none !important' );
528				$( 'button.seats-modal-checkout' ).hide();
529			} );
530
531			// hide ADA
532			themeCheckout.ADAModeOff.click( function () {
533				themeCheckout.ADAContainer.hide();
534				themeCheckout.seatmapContainer.show();
535				themeCheckout.ADAMode = false;
536				themeCheckout.seatsADAList.val( '' );
537				themeCheckout.$checkoutSeatsModal.text( 'Checkout' );
538				themeCheckout.ADAModeOn.show();
539				themeCheckout.$siteHeader.show();
540				themeCheckout.$siteFooter.css( 'display', 'flex' );
541				$( 'button.seats-modal-checkout' ).show();
542				themeCheckout.showSeatsModal();
543			} );
544
545			// handle choose seats from ADA selector
546			themeCheckout.seatsADAList.change( function () {
547				themeCheckout.selectedADASeat     = [];
548				themeCheckout.selectedADASeatName = [];
549				themeCheckout.confirmADA          = '';
550				var selectOptionQt                = 0;
551				if ( themeCheckout.seatsADAList.val() ) {
552					selectOptionQt = themeCheckout.seatsADAList.val().length;
553				}
554
555				if ( selectOptionQt > ticketLimitOnWebsite ) {
556					themeCheckout.seatsADAList.val( themeCheckout.lastValidSelection );
557					themeCheckout.$submitError.attr( {
558						role: 'alert',
559						tabindex: -1
560					} ).text( `You cannot select more than ${ticketLimitOnWebsite} seats.` );
561					themeCheckout.selectedADASeat = themeCheckout.lastValidSelection;
562				} else {
563					$.each( $( themeCheckout.seatsADAList ).find( ":selected" ), function () {
564						themeCheckout.selectedADASeat.push( $( this ).val() );
565						themeCheckout.selectedADASeatName.push( $( this ).data( 'name-seat' ) );
566						themeCheckout.$checkoutSeatsModal.text( "Reserve Seats:" + themeCheckout.selectedADASeatName );
567						themeCheckout.clearErrors();
568					} );
569					themeCheckout.lastValidSelection = $( this ).val();
570					themeCheckout.clearErrors();
571				}
572
573				if ( selectOptionQt > 0 ) {
574					$( 'button.seats-modal-checkout' ).show();
575				} else {
576					$( 'button.seats-modal-checkout' ).hide();
577					themeCheckout.$checkoutSeatsModal.text( 'Checkout' );
578				}
579			} );
580
581			// submit reserve seats if pressed enter key in seatslist
582			$( document ).keypress( function ( e ) {
583				if ( e.which === 13 && themeCheckout.seatsADAList.val() && themeCheckout.seatsADAList.is( ':focus' ) ) {
584					themeCheckout.$checkoutSeatsModal.click();
585				}
586			} );
587
588			// Some simple AJAX loading-state functionality to help prevent form submissions while things are still loading.
589			themeCheckout.$checkout.on( 'checkout-start-loading', function () {
590				themeCheckout.$checkout.addClass( 'checkout--is-loading' );
591			} );
592
593			themeCheckout.$checkout.on( 'checkout-stop-loading', function () {
594				themeCheckout.$checkout.removeClass( 'checkout--is-loading' );
595			} );
596
597			// Set correct value to showtimes select on page loaded from bfcache
598			window.addEventListener( 'pageshow', ( event ) => {
599				if ( event.persisted ) {
600					themeCheckout.recheckShowtimeSelect();
601				}
602			} );
603
604			// check if current select value correlates with the current location
605			themeCheckout.$rsShowtimeSelect.ready( function () {
606				themeCheckout.recheckShowtimeSelect();
607			} );
608
609			// show or hide seatmap depending on what page customer is on
610			window.addEventListener( 'popstate', function ( event ) {
611				if ( themeCheckout.checkoutSubmitted ) {
612					history.pushState( null, '', window.location.href );
613					return;
614				}
615
616				const element   = themeCheckout.$openSeatsModal;
617				const isVisible = element.length && element.is( ':visible' );
618
619				// if `hasSelectedSeats` is set in state this only means that this page is pulled from bfcache after tickets were reserved
620				if ( event.state && event.state.hasSelectedSeats && themeCheckout.order.showtime ) {
621
622					$( 'html, body' ).css( { overflow: 'visible' } );
623
624					const $timeContainer = $( '.reservation-countdown' );
625					var hasExpiredTimer  = false;
626					$timeContainer.each( function () {
627						const isExpired = $( this ).html().indexOf( '00:00' ) !== -1 || $( this ).html().indexOf( '00:-' ) !== -1;
628						if ( isExpired ) {
629							hasExpiredTimer = true;
630						}
631					} );
632
633					const $ticketsTypeSelectors = $( '.seat-ticket-type .select-ticket-type' ).not( '.hidden' );
634
635					if ( isVisible || hasExpiredTimer ) {
636						if ( hasExpiredTimer ) {
637							themeCheckout.$selectedSeats   = [];
638							themeCheckout.$reservedOrderId = null;
639							themeCheckout.updateSeatsView( [] );
640							themeCheckout.changeTextCheckoutButton( themeCheckout.$selectedSeats );
641						}
642						themeCheckout.getSeatsData();
643					} else {
644						// Show checkout page without seats modal only if there are tickets with not outdated reservation
645						if ( $ticketsTypeSelectors.length ) {
646							themeCheckout.hideSeatsModal();
647						}
648					}
649
650					// if there are expired countdowns on pages pulled from bfcache, hide them
651					$timeContainer.each( function () {
652						if ( $( this ).html().indexOf( '00:00' ) !== -1 || $( this ).html().indexOf( '00:-' ) !== -1 ) {
653							$( this ).parent().hide();
654						}
655					} );
656
657
658				}
659			}, false );
660
661			window.themeUx.$document.on( 'filmbot-change-discount-option', function () {
662				themeCheckout.startApplyingDiscountCode( { reapplyDiscountCode: true } );
663			} );
664
665			// Prevent page leave if checkout is already submitted
666			window.addEventListener( 'beforeunload', themeCheckout.preventPageLeave );
667			window.onbeforeunload = themeCheckout.preventPageLeave; // Safari sometimes prefers this
668		},
669		/**
670		 * Prevent page leave if checkout is already submitted
671		 *
672		 * @param event
673		 */
674		preventPageLeave: function ( event ) {
675			if ( themeCheckout.checkoutSubmitted ) {
676				event.preventDefault();
677				event.returnValue = '';
678				return '';
679			}
680		},
681		clearErrors: function () {
682			themeCheckout.$submitError.attr( {
683				role: '',
684				tabindex: ''
685			} ).text( '' );
686		},
687		hideSeatsModal: function () {
688			themeCheckout.clearErrors();
689			themeCheckout.$checkout.show();
690			themeCheckout.$sidebar.attr( {
691				'aria-hidden': false,
692				'role': 'banner'
693			} ).show();
694			themeCheckout.$siteFooter.attr( {
695				'aria-hidden': false
696			} ).show();
697			themeCheckout.$siteHeader.attr( {
698				'aria-hidden': false,
699				'role': 'banner'
700			} );
701			themeCheckout.$seatsModal.hide();
702			$( "html, body" ).removeClass( 'lock-scroll' ).animate( { scrollTop: 0 }, "fast" );
703			$( '#primary-nav' ).attr( 'role', 'navigation' ).show();
704
705			$( 'body' ).trigger( 'filmbot-seats-modal-hide' );
706		},
707		showSeatsModal: function () {
708			themeCheckout.$overlayMessage.hide();
709			themeCheckout.$seatsModal.show();
710			themeCheckout.$checkout.hide();
711			themeCheckout.$sidebar.attr( {
712				'aria-hidden': true,
713				'role': ''
714			} );
715			themeCheckout.$siteFooter.attr( {
716				'aria-hidden': true,
717				'role': ''
718			} ).hide();
719			themeCheckout.$siteHeader.attr( {
720				'aria-hidden': true,
721				'role': ''
722			} );
723			// hide menu if not mobile and tablet
724			if ( !themeCheckout.$isTablet.matches ) {
725				themeCheckout.$sidebar.hide();
726				themeCheckout.$siteHeader.hide();
727			}
728			$( "html, body" ).addClass( 'lock-scroll' );
729			$( '#primary-nav' ).attr( 'role', '' ).hide();
730		},
731		seatClickHandler: function ( items ) {
732			themeCheckout.$selectedSeats = items;
733			themeCheckout.changeTextCheckoutButton( items );
734			if ( items.length === ticketLimitOnWebsite ) {
735				themeCheckout.$submitError.text( `You cannot select more than ${ticketLimitOnWebsite} seats.` );
736			} else {
737				themeCheckout.clearErrors();
738			}
739		},
740		initializeSeatsView: function ( seatsData, clearErrors = true ) {
741			themeCheckout.$seatsData = seatsData;
742
743			if ( clearErrors ) {
744				// Do not show payment error if customer changes seats
745				themeCheckout.$submitError.text( '' );
746			}
747
748			themeCheckout.showSeatsModal();
749
750			// destroy if to re-render
751			if ( themeCheckout.$hall ) {
752				themeCheckout.$hall.destroy();
753				$( '.hall-container' ).html( '<div class="hall-wrapper"><canvas id="hall"></canvas></div>' );
754			}
755
756			// initialize seats map component
757			if ( themeCheckout.$seatsData && !jQuery.isEmptyObject( themeCheckout.$seatsData ) ) {
758
759				// A hack to show reserved seats instead of selected ones
760				var selectedSeats   = themeCheckout.$reservedSeats.length ? themeCheckout.$reservedSeats : themeCheckout.$selectedSeats;
761				themeCheckout.$hall = new window.FilmbotHall( themeCheckout.$seatsData, selectedSeats, {
762					onSelect: themeCheckout.seatClickHandler,
763					onDeselect: themeCheckout.seatClickHandler,
764					selectionLimit: ticketLimitOnWebsite,
765					canvasWidth: 800,
766					canvasHeight: 300,
767					canvasBg: window.themeSettings.theme_mods.rs_seatmap_background_color,
768					stroke: window.themeSettings.theme_mods.rs_seat_color,
769					fill: window.themeSettings.theme_mods.rs_seat_color,
770					disabledColor: window.themeSettings.theme_mods.rs_seat_unavailable_color,
771					selector: 'hall'
772				}, themeCheckout.$reservedSeats );
773				themeCheckout.$hall.render();
774			}
775			themeCheckout.getShortSeatMapList();
776		},
777		initializeSeatsADAList: function ( seatsData ) {
778			themeCheckout.$seatsData = seatsData;
779
780			// render select form for ADA
781			$.each( seatsData.sections, function ( index, section ) {
782				var legendSeats = seatsData.legend;
783
784				function getTitleSeat( seat_type ) {
785					try {
786						return legendSeats.find( seatType => seatType.seat_type === seat_type ).title;
787					} catch ( error ) {
788						return 'Standard'; // default value
789					}
790				}
791
792				if ( section ) {
793					$.each( section.rows, function ( index, row ) {
794						if ( row ) {
795							$.each( row.seats, function ( index, seatsList ) {
796								if ( seatsList['seat_type'] !== 'no-seat' ) {
797									var availabilitySeat        = seatsList['data'] ? seatsList['data']['available'] : undefined;
798									var availabilityAttribute   = '';
799									var seatName                = seatsList['display_id'];
800									var seatType                = seatsList['seat_type'];
801									var seatTypeTitle           = getTitleSeat( seatType );
802									var seatExtra               = '';
803									var confirm_ada_description = '';
804									var seatAddress             = seatsList['seat_address'];
805									var seatNameAttribute       = `data-name-seat="${seatName}"`;
806
807									if ( 'undefined' === typeof availabilitySeat ) {
808										availabilitySeat = '';
809									} else {
810										availabilitySeat      = ' - Reserved';
811										availabilityAttribute = ' disabled="true" ';
812									}
813
814									if ( seatsList['confirm_message'] ) {
815										confirm_ada_description = `. ${seatsList['confirm_message']}`;
816									}
817
818									if ( seatsList['extra'] ) {
819										seatExtra += ` - ${seatsList['extra']}`;
820									}
821
822									var rowName       = row.display_id ? `Row ${row.display_id} ` : '';
823									var optionContent = `${rowName}Seat ${seatName}: ${seatTypeTitle}${seatExtra}${availabilitySeat}${confirm_ada_description}`;
824
825									themeCheckout.seatsADAList.append(
826										`<option role="option" ${seatNameAttribute} aria-label="${optionContent}" ${availabilityAttribute} value="${seatAddress}">${optionContent}</option>`
827									);
828								}
829							} );
830						}
831					} );
832				}
833			} );
834		},
835		timerInterval: false,
836		showReservationExpiredMessage: function () {
837			themeCheckout.$overlayMessage.empty().append( '<h3>Sorry!</h3>' );
838			themeCheckout.$overlayMessage.append( '<p>Your reservation for ' + $( '.showtime-date' ).text() + ' has expired.</p>' );
839			themeCheckout.$overlayMessage.show();
840			$( 'html' ).css( { overflow: 'hidden' } );
841			if ( !$( 'body' ).hasClass( 'admin-bar' ) ) {
842				$( 'body' ).css( { overflow: 'hidden' } );
843			}
844			themeCheckout.hideSeatsModal();
845
846			themeCheckout.hasReservationExpired = true;
847		},
848		checkIfTimerHasExpired: function () {
849			var countdownText = $( '.reservation-countdown' );
850
851			// Timer is not present on the page.
852			if ( !countdownText.length ) {
853				return false;
854			}
855
856			var countdownTime   = countdownText.text();
857			// Check if the countdown timer has expired (has negative time remaining).
858			var expirationRegex = /0-?\d+:0-\d+/g;
859			return expirationRegex.test( countdownTime );
860		},
861		showReservationExpiredMessageIfTImerHasExpired: function () {
862			// Check if the timer has expired and show the message if it has in case the user has not submitted the checkout.
863			if ( themeCheckout.$reservedOrderId && themeCheckout.checkIfTimerHasExpired() ) {
864				themeCheckout.showReservationExpiredMessage();
865				return true;
866			}
867			return false;
868		},
869		startTimer: function () {
870			var duration       = 60 * 5; // Hard-coded 5-minute timer.
871			var suffix         = ' minutes';
872			var displayWrapper = $( '.reservation-countdown-message' );
873			var display        = $( '.reservation-countdown' );
874			var start          = Date.now();
875			var diff;
876			var minutes;
877			var seconds;
878
879			displayWrapper.show();
880
881			function timer() {
882				// get the number of seconds that have elapsed since startTimer() was called
883				diff = duration - (((Date.now() - start) / 1000) | 0);
884
885				if ( diff <= 0 ) {
886					themeCheckout.stopTimer();
887
888					if ( !themeCheckout.checkoutSubmitted ) {
889						themeCheckout.showReservationExpiredMessage();
890					}
891				}
892
893				if ( diff < 60 ) {
894					suffix = ' seconds';
895				}
896
897				// does the same job as parseInt truncates the float
898				minutes = (diff / 60) | 0;
899				seconds = (diff % 60) | 0;
900
901				minutes = minutes < 10 ? '0' + minutes : minutes;
902				seconds = seconds < 10 ? '0' + seconds : seconds;
903
904				display.text( minutes + ':' + seconds + suffix );
905
906				$.each( displayWrapper, function ( i, el ) {
907					var $wrapper = $( el );
908					var $alert   = $wrapper.next( '.reservation-countdown-alert' );
909
910					if ( 'undefined' === typeof $alert ) {
911						return;
912					}
913
914					$alert.text( $wrapper.text() );
915				} );
916			}
917
918			// we don't want to wait a full second before the timer starts
919			timer();
920
921			themeCheckout.timerInterval = setInterval( timer, 1000 );
922		},
923		stopTimer: function () {
924			clearInterval( themeCheckout.timerInterval );
925		},
926		triggerSentryWithExtras: function ( message, extras ) {
927			// Log a Sentry error
928			if ( typeof Sentry !== 'undefined' ) {
929				Sentry.withScope( function ( scope ) {
930					scope.setLevel( 'error' );
931
932					if ( extras ) {
933						scope.setExtras( extras );
934					}
935					Sentry.captureMessage( message );
936				} );
937			}
938		},
939		getSeatsData: function ( clearErrors = true ) {
940
941			// Do not allow to open seats popup if checkout is already submitted
942			if ( themeCheckout.checkoutSubmitted ) {
943				return;
944			}
945
946			var data = {
947				action: 'filmbot_get_auditorium_seats',
948				_ajax_nonce: $( '#filmbot_checkout_nonce' ).val(),
949				showtime_id: themeCheckout.order.showtime
950			};
951
952			var seatMapsFetched = $.when( $.ajax(
953				{
954					url: window.ajaxurl || window.themeSettings.ajaxUrl,
955					method: 'POST',
956					timeout: 60000,
957					data: data,
958					success: function ( response ) {
959						if ( !!response.sections ) {
960							themeCheckout.initializeSeatsView( response, clearErrors );
961							themeCheckout.initializeSeatsADAList( response );
962							console.info( '[ Fetch seats success ] ', response );
963						}
964					},
965					error: function ( xhr, status, error ) {
966						themeCheckout.$submitError.attr( 'role', 'alert' ).text( 'An error occurred, please wait a few minutes and refresh the page.' ).focus();
967
968						console.error( xhr, status, error );
969
970						themeCheckout.triggerSentryWithExtras( '[ Fetch seats error ] ' + error, {
971							xhr: xhr,
972							status: status
973						} );
974					}
975				}
976			) );
977
978			seatMapsFetched.then( function () {
979				if (
980					'undefined' === typeof window.themeSettings.reservedSeating ||
981					'string' !== typeof window.themeSettings.reservedSeating.socialDistancing.infoMsg ) {
982					return;
983				}
984
985				// A flag for ensuring this info message only displays once, not on each seat map rendering.
986				if ( 'true' === window.themeSettings.reservedSeating.socialDistancing.infoMsgAccepted ) {
987					return;
988				}
989
990				window.Swal.fire(
991					{
992						text: window.themeSettings.reservedSeating.socialDistancing.infoMsg,
993						showCancelButton: false,
994						confirmButtonText: "Continue",
995						confirmButtonColor: ""
996					}
997				).then( ( result ) => {
998					// On "accepting" the info message (closing it), set this flag to prevent the message showing again.
999					window.themeSettings.reservedSeating.socialDistancing.infoMsgAccepted = 'true';
1000				} );
1001			} );
1002		},
1003		reserveSeats: function () {
1004			if ( themeCheckout.$inRequest ) return;
1005
1006			if ( !themeCheckout.$selectedSeats.length && !themeCheckout.ADAMode ) {
1007				themeCheckout.$submitError.attr( 'role', 'alert' ).text( ' Please select seats.' );
1008				themeCheckout.getSeatsData();
1009				return false;
1010			}
1011
1012			var seats                  = [];
1013			var containerSelectedSeats = themeCheckout.$selectedSeats;
1014			if ( themeCheckout.ADAMode ) {
1015				containerSelectedSeats = themeCheckout.selectedADASeat;
1016				themeCheckout.ADAModeOff.hide();
1017			}
1018			themeCheckout.ADAModeOn.hide();
1019			$.each( containerSelectedSeats, function ( index, seat ) {
1020				seats.push( { "seat": seat } );
1021			} );
1022
1023			themeCheckout.order.donation = themeCheckout.roundDecimalValue( themeCheckout.currentDonationAmount );
1024
1025			var data = {
1026				action: 'filmbot_reserve_seats',
1027				_ajax_nonce: $( '#filmbot_checkout_nonce' ).val(),
1028				order: {
1029					reservations: [
1030						{
1031							"showtime_id": themeCheckout.order.showtime,
1032							"requested_tickets": seats
1033						}
1034					]
1035				}
1036			};
1037
1038			if ( themeCheckout.$reservedOrderId ) {
1039				data['action']   = 'filmbot_change_reserved_seats';
1040				data['order_id'] = themeCheckout.$reservedOrderId;
1041			}
1042
1043			themeCheckout.$inRequest = true;
1044			themeCheckout.$checkoutSeatsModal.attr( 'disabled', 'disabled' ).html( 'Reserving seats...' );
1045
1046			$.ajax( {
1047				url: window.ajaxurl || window.themeSettings.ajaxUrl,
1048				method: 'POST',
1049				timeout: 60000,
1050				data: data,
1051				success: function ( response ) {
1052					if ( response.success === false ) {
1053						themeCheckout.triggerSentryWithExtras( '[ Reserve seats error ] ' + response.data[0], {
1054							orderId: response.order_id || themeCheckout.$reservedOrderId || null,
1055							response: response
1056						} );
1057
1058						themeCheckout.$submitError.attr( {
1059							role: 'alert',
1060							tabindex: -1
1061						} ).text( response.data[0] );
1062						themeCheckout.getSeatsData( false );
1063						themeCheckout.$checkout.show();
1064						return false;
1065					}
1066
1067					// Check if the timer has expired before proceeding and show an error message if it has.
1068					if ( themeCheckout.showReservationExpiredMessageIfTImerHasExpired() ) {
1069						return false;
1070					}
1071					if ( !themeCheckout.$reservedOrderId ) themeCheckout.startTimer();
1072					themeCheckout.$reservedOrderId = response.order_id;
1073
1074					// hide showtime picker, just show showtime
1075					themeCheckout.$rsShowtimePicker.hide();
1076					themeCheckout.$rsSelectedShowtime.show();
1077
1078					if ( !response.success ) {
1079						themeCheckout.$submitError.attr( {
1080							role: 'alert',
1081							tabindex: -1
1082						} ).text( response.data[0] );
1083						themeCheckout.getSeatsData();
1084					} else {
1085						console.info( '[ Reserve seats success ] ', response );
1086
1087						themeCheckout.updateSeatsView( response.reservations );
1088
1089						// show Cancel button for Change usage and hide modal
1090						themeCheckout.$cancelSeatsModal.show();
1091
1092						// before hiding modal set it as separated entity in browser history
1093						if ( containerSelectedSeats.length ) {
1094							history.replaceState( {
1095								hasSelectedSeats: true
1096							}, null, window.location.href );
1097							history.pushState( {
1098								hasSelectedSeats: true
1099							}, null, window.location.href );
1100						}
1101						themeCheckout.hideSeatsModal();
1102
1103						if ( themeCheckout.$discountCode.val() !== '' ) {
1104							themeCheckout.applyDiscountCode();
1105						}
1106					}
1107				},
1108				error: function ( xhr, status, error ) {
1109					themeCheckout.getSeatsData();
1110					themeCheckout.$submitError.attr( 'role', 'alert' ).text( 'There was an error reserving seats, please try again.' );
1111
1112					console.error( xhr, status, error );
1113
1114					themeCheckout.triggerSentryWithExtras( '[ Reserve seats error ] ' + error, {
1115						xhr: xhr,
1116						status: status
1117					} );
1118				},
1119				complete: function () {
1120					themeCheckout.$inRequest = false;
1121					themeCheckout.$checkoutSeatsModal.attr( 'disabled', false );
1122					themeCheckout.changeTextCheckoutButton( themeCheckout.$selectedSeats );
1123				}
1124			} );
1125		},
1126		changeReservedSeatTicketType: function ( e ) {
1127			if ( themeCheckout.$inRequest ) return;
1128
1129			var selectedTicketType = $( e.target ).val(),
1130				selectedSeat       = $( e.target ).data( 'seat' );
1131
1132			themeCheckout.order.donation = themeCheckout.roundDecimalValue( themeCheckout.currentDonationAmount );
1133
1134			var data = {
1135				action: 'filmbot_change_reserved_seat_ticket_types',
1136				_ajax_nonce: $( '#filmbot_checkout_nonce' ).val(),
1137				order_id: themeCheckout.$reservedOrderId,
1138				order: {
1139					reservations: [
1140						{
1141							"showtime_id": themeCheckout.order.showtime,
1142							"requested_tickets": [
1143								{
1144									"seat": selectedSeat,
1145									"ticket_type": selectedTicketType
1146								}
1147							]
1148						}
1149					]
1150				}
1151			};
1152
1153			themeCheckout.$inRequest = true;
1154			themeCheckout.disableForm( 'UPDATING TOTAL...' );
1155
1156			$.ajax( {
1157				url: window.ajaxurl || window.themeSettings.ajaxUrl,
1158				method: 'POST',
1159				timeout: 60000,
1160				data: data,
1161				success: function ( response ) {
1162					if ( !response.success ) {
1163						themeCheckout.triggerSentryWithExtras( '[ Change ticket type error ] ' + response.data[0], {
1164							orderId: themeCheckout.$reservedOrderId || null,
1165							response: response
1166						} );
1167
1168						console.error( '[ Change ticket type error ]', response.data );
1169						themeCheckout.$submitError.attr( {
1170							role: 'alert',
1171							tabindex: -1
1172						} ).text( response.data[0] );
1173					} else {
1174						themeCheckout.updateSeatsView( response.reservations );
1175
1176						console.info( '[ Change ticket type success ] ', response );
1177
1178						themeCheckout.applyDiscountCode();
1179						themeCheckout.maybeReapplyGiftCard();
1180					}
1181				},
1182				error: function ( xhr, status, error ) {
1183					themeCheckout.$submitError.attr( 'role', 'alert' ).text( 'There was an error changing ticket type, please try again.' );
1184
1185					console.error( xhr, status, error );
1186
1187					themeCheckout.triggerSentryWithExtras( '[ Change ticket type error ] ' + error, {
1188						xhr: xhr,
1189						status: status
1190					} );
1191				},
1192				complete: function () {
1193					themeCheckout.$inRequest = false;
1194					themeCheckout.enableForm( {} );
1195				}
1196			} );
1197		},
1198		clearSeatsView: function () {
1199			themeCheckout.$seatTicketTypeSelect.off( 'change' );
1200			$( '.seat-ticket-type .select-ticket-type' ).not( '.hidden' ).remove();
1201		},
1202		getSeatProperties: function ( seatAddress ) {
1203			if ( jQuery.isEmptyObject( themeCheckout.seatPropertiesCache[seatAddress] ) ) {
1204				var seatMapping    = seatAddress.split( "-" ),
1205					sectionId      = parseInt( seatMapping[0], 10 ) - 1,
1206					rowId          = parseInt( seatMapping[1], 10 ) - 1,
1207					seatId         = parseInt( seatMapping[2], 10 ) - 1,
1208					seat           = themeCheckout.$seatsData['sections'][sectionId]['rows'][rowId]['seats'][seatId],
1209					seatProperties = {
1210						seatDisplayId: seat.display_id,
1211						seatAllowedTicketTypes: seat.data.allowed_ticket_types,
1212						confirmMessage: seat.confirm_message || false
1213					};
1214
1215				themeCheckout.seatPropertiesCache[seatAddress] = seatProperties;
1216			}
1217			return themeCheckout.seatPropertiesCache[seatAddress];
1218		},
1219		updateSeatsView: function ( reservations ) {
1220			themeCheckout.clearSeatsView();
1221			themeCheckout.$reservedSeats = [];
1222
1223			$.each( reservations, function ( index, data ) { // backend returns reservations within a single element array
1224				$.each( data, function ( index, data ) {
1225					// available properties: 'seat' (ex. "1-2-1"), 'ticket_type' (ex. "general-admission")
1226
1227					themeCheckout.$reservedSeats.push( data.seat );
1228
1229					// Find seat display ID.
1230					var seatProperties = themeCheckout.getSeatProperties( data.seat );
1231
1232					var seatElement = $( '.seat-ticket-type .select-ticket-type.hidden' ).clone();
1233					var seatSelect  = seatElement.find( '.select-container' ).find( 'select' );
1234
1235					$.each( seatSelect.find( 'option' ), function ( i, option ) {
1236						if ( !(seatProperties.seatAllowedTicketTypes.indexOf( $( option ).data( 'ticket_type' ).ID ) > -1) ) {
1237							$( option ).prop( 'disabled', true );
1238						}
1239					} );
1240
1241					seatSelect.val( data.ticket_type ).data( 'seat', data.seat );
1242
1243					if ( '_blocked' !== data.ticket_type ) {
1244						seatElement.find( 'label' ).html( "Seat: <br />" + seatProperties.seatDisplayId );
1245						seatElement.find( '.select-ticket-type-aria-description' ).attr( 'id', `select-ticket-type-desc-${seatProperties.seatDisplayId}` ).html( `Select ticket type for seat ${seatProperties.seatDisplayId}` );
1246						seatElement.find( '.seat-ticket-type-select' ).attr( 'aria-describedby', `select-ticket-type-desc-${seatProperties.seatDisplayId}` );
1247						seatElement.appendTo( '.seat-ticket-type' ).removeClass( 'hidden' );
1248					}
1249				} );
1250			} );
1251
1252			// Refresh props and reattach select change handler
1253			themeCheckout.refreshableProps();
1254			themeCheckout.$seatTicketTypeSelect.change( themeCheckout.changeReservedSeatTicketType );
1255
1256			// Update checkout prices
1257			themeCheckout.toggleActiveCheckoutBySeats();
1258		},
1259		attachFauxselect: function () {
1260			// open the styled select dropdown
1261			window.themeUx.$showList.on( 'click', function ( e ) {
1262				window.themeUx.openDropdownList( e );
1263			} );
1264
1265			// close the styled select dropdown
1266			window.themeUx.$document.on( 'click', function ( e ) {
1267				window.themeUx.closeDropdownList( e );
1268			} );
1269
1270			// select an item in the styled select dropdown
1271			window.themeUx.$list.find( 'li' ).click( function ( e ) {
1272				if ( $( e.target ).hasClass( 'disabled' ) )
1273					return;
1274				window.themeUx.selectDropdownListQuantity( e );
1275			} );
1276
1277			// update value of styled select dropdown on native dropdown change event
1278			window.themeUx.$styledSelect.change( function ( e ) {
1279				window.themeUx.updateDummySelect( e );
1280			} );
1281
1282		},
1283		toggleActiveCheckout: function () {
1284
1285			themeCheckout.$checkoutContainer.removeClass( 'checkout-active' );
1286
1287			$.each( themeCheckout.$quantitySelect, function ( i, qtySelect ) {
1288				if ( parseInt( $( qtySelect ).val(), 10 ) > 0 ) {
1289					themeCheckout.$checkoutContainer.addClass( 'checkout-active' );
1290
1291					themeCheckout.$discountCodeError.css( 'visibility', 'hidden' );
1292					themeCheckout.$discountCodeError.parent().removeClass( 'large-error-message' );
1293					if ( themeCheckout.$emailInput.val() ) {
1294						themeCheckout.checkEmailMaybeApplyMembershipCode();
1295					}
1296				}
1297			} );
1298
1299			// If checkout is deactivated (e.g. all quantities are zeroed out), reset payment method selection.
1300			if ( !themeCheckout.$checkoutContainer.hasClass( 'checkout-active' ) ) {
1301				themeCheckout.selectPaymentMethodType();
1302			}
1303
1304			themeCheckout.updateQuantity();
1305		},
1306		toggleActiveCheckoutBySeats: function () {
1307
1308			themeCheckout.$checkoutContainer.removeClass( 'checkout-active' );
1309
1310			if ( themeCheckout.$reservedOrderId ) {
1311				themeCheckout.$checkoutContainer.addClass( 'checkout-active' );
1312			}
1313
1314			themeCheckout.updateQuantityBySeats();
1315		},
1316		manageQtysAvailable: function () {
1317
1318			var $qtyAvailable = $( '#qty-available' )
1319
1320			// if we're not low on tickets, don't do anything
1321			if ( !$qtyAvailable.length )
1322				return;
1323
1324			var totalQty               = 0,
1325				qtyRemaining,
1326				qtyAvailable           = parseInt( $qtyAvailable.text(), 10 ),
1327				$dummySelectContainers = $( '.ticket-quantity .dummy-select-container' );
1328
1329			$.each( themeCheckout.$quantitySelect, function ( i, qtySelect ) {
1330				totalQty += parseInt( $( qtySelect ).val(), 10 )
1331			} );
1332
1333			if ( totalQty > 0 ) {
1334				$( '.ticket-quantity' ).removeClass( 'no-quantity' );
1335			}
1336
1337			qtyRemaining = (qtyAvailable - totalQty);
1338
1339			// reset all ticket quantities to be enabled
1340			themeCheckout.$quantitySelect.find( 'option' ).prop( 'disabled', false );
1341			$dummySelectContainers.find( 'li' ).removeClass( 'disabled' );
1342
1343			// disable ticket quantities based on how many are left
1344			$.each( themeCheckout.$quantitySelect, function ( i, qtySelect ) {
1345				var $qtySelect               = $( qtySelect ),
1346					selectedIndex            = $qtySelect.prop( 'selectedIndex' ),
1347					$qtySelectDummyContainer = $qtySelect.closest( '.select-container' ).next( '.dummy-select-container' ),
1348					$qtySelectDummy          = $qtySelectDummyContainer.find( '.selected-quantity' ),
1349					maxSelectable            = qtyRemaining + selectedIndex;
1350				$qtySelect.find( 'option:gt(' + (maxSelectable) + ')' ).prop( 'disabled', true );
1351				$qtySelectDummyContainer.find( 'li:gt(' + (maxSelectable) + ')' ).addClass( 'disabled' );
1352				$qtySelectDummy.html( selectedIndex );
1353			} )
1354		},
1355		/**
1356		 * Rounds a decimal value to the nearest hundredth
1357		 *
1358		 * @param value
1359		 * @return {number|*}
1360		 */
1361		roundDecimalValue: function ( value ) {
1362			if ( isNaN( value ) ) {
1363				return value;
1364			}
1365
1366			return Math.round( value * 100 ) / 100;
1367		},
1368		updateQuantity: function () {
1369
1370			var totalTickets = 0;
1371			var bookingFee   = 0;
1372			var totalPrice   = 0;
1373
1374			themeCheckout.order.tickets = {};
1375
1376			$.each( themeCheckout.$quantitySelect, function ( i, qtySelect ) {
1377				var $qtySelect         = $( qtySelect );
1378				var $qtySelectDummy    = $( qtySelect ).closest( '.select-container' ).next( '.dummy-select-container' ).find( '.selected-quantity' );
1379				var ticketQty          = parseInt( $qtySelect.val(), 10 );
1380				var ticketType         = $qtySelect.data( 'ticket_type' );
1381				var $ticketInfo        = $( '.tickets_' + ticketType.name );
1382				var ticketTypeSubtotal = ticketQty * ticketType.price;
1383
1384				//update styled dropdown
1385				$qtySelectDummy.html( ticketQty );
1386
1387				totalTickets += ticketQty;
1388				totalPrice += ticketTypeSubtotal;
1389
1390				if ( ticketType.price > 0 ) {
1391					bookingFee += ticketQty * BOOKING_FEE;
1392				}
1393
1394				themeCheckout.order.tickets[ticketType.name] = ticketQty;
1395
1396				$ticketInfo
1397					.find( '.quantity' ).text( ticketQty ).end()
1398					.find( '.subtotal' ).text( (ticketTypeSubtotal).toFixed( 2 ) );
1399
1400				if ( ticketQty > 0 ) {
1401					$ticketInfo.addClass( 'active' );
1402				} else {
1403					$ticketInfo.removeClass( 'active' );
1404				}
1405			} );
1406
1407			totalPrice += bookingFee;
1408
1409			totalPrice += themeCheckout.currentDonationAmount;
1410			themeCheckout.order.donation = themeCheckout.roundDecimalValue( themeCheckout.currentDonationAmount );
1411
1412			if ( themeCheckout.order.discount ) {
1413				totalPrice -= themeCheckout.order.discount;
1414			}
1415
1416			$( '.booking_fee_total' ).text( bookingFee.toFixed( 2 ) );
1417
1418			// Maybe subtract the amount the gift card can cover from the overall total.
1419			var totalPriceBeforeGiftCard = totalPrice;
1420			if ( 'undefined' !== typeof themeCheckout.order.giftCardTotal ) {
1421
1422				// Before modifying totalPrice, check giftCardTotal against it; if it's > 0 at this point, it means a donation was added or a promo code removed since the gift card was applied. So we need to make sure payment method is gc_partial, not gc.
1423				if (
1424					0 !== themeCheckout.order.giftCardTotal &&
1425					(totalPrice - themeCheckout.order.giftCardTotal) > 0
1426				) {
1427					themeCheckout.paymentMethod = 'gc_partial';
1428				}
1429
1430				totalPrice -= themeCheckout.order.giftCardTotal;
1431			}
1432
1433			// In some cases like adding/removing discount codes to an order that has some or all of its cost covered by a gift card, totalPrice can be a negative value here. This check ensures the actual total never goes below $0.
1434			if ( totalPrice < 0 ) {
1435				totalPrice = 0;
1436			}
1437
1438			themeCheckout.updateOrderTotal( totalPrice );
1439			themeCheckout.order.originalTotal = themeCheckout.roundDecimalValue( totalPrice ); // themeCheckout.order.total can be modified by discount codes, gift cards, etc. This original_total value is only modified on ticket quantity changes, and is useful for resetting UI state as discount codes, gift cards, etc. are added and removed.
1440			themeCheckout.order.bookingFee    = themeCheckout.roundDecimalValue( bookingFee );
1441
1442			$( '.total_purchase_price' ).text( themeCheckout.moneyFormat( totalPrice ) );
1443
1444			themeCheckout.maybeToggleCreditCardFields();
1445
1446			// Perform a similar paymentMethod check as above, where if a donation's been added or promo code removed since applying a gift card, we ensure the payment method is gc_partial, not gc.
1447			// Uses totalPriceBeforeGiftCard since order.total is already gift-card-reduced here.
1448			if ( 'undefined' !== typeof themeCheckout.order.giftCardTotal ) {
1449				if (
1450					0 !== themeCheckout.order.giftCardTotal &&
1451					(totalPriceBeforeGiftCard - themeCheckout.order.giftCardTotal) > 0
1452				) {
1453					themeCheckout.paymentMethod = 'gc_partial';
1454				}
1455			}
1456		},
1457		updateQuantityBySeats: function () {
1458			var totalTickets   = 0;
1459			var ticketTypesQty = [];
1460			var bookingFee     = 0;
1461			var totalPrice     = 0;
1462
1463			themeCheckout.order.tickets = {};
1464
1465			$.each( themeCheckout.$seatTicketTypeSelect, function ( i, ticketTypeSelect ) {
1466				var ticketType = $( 'option:selected', ticketTypeSelect ).data( 'ticket_type' );
1467
1468				ticketType['qty']               = ticketTypesQty[ticketType.name] ? ticketTypesQty[ticketType.name]['qty'] + 1 : 1;
1469				ticketTypesQty[ticketType.name] = ticketType;
1470			} );
1471
1472			$( '.tickets_quantity_subtotal' ).removeClass( 'active' );
1473
1474			for ( var key in ticketTypesQty ) {
1475				if ( ticketTypesQty.hasOwnProperty( key ) ) {
1476					var ticketType         = ticketTypesQty[key];
1477					var $ticketInfo        = $( '.tickets_' + ticketType.name );
1478					var ticketTypeSubtotal = ticketType.qty * ticketType.price;
1479
1480					totalTickets += ticketType.qty;
1481					totalPrice += ticketTypeSubtotal;
1482					if ( ticketType.price > 0 ) {
1483						bookingFee += ticketType.qty * BOOKING_FEE;
1484					}
1485
1486					themeCheckout.order.tickets[ticketType.name] = ticketType.qty;
1487
1488					$ticketInfo
1489						.find( '.quantity' ).text( ticketType.qty ).end()
1490						.find( '.subtotal' ).text( (ticketTypeSubtotal).toFixed( 2 ) );
1491
1492					if ( ticketType.qty > 0 ) {
1493						$ticketInfo.addClass( 'active' );
1494					}
1495				}
1496			}
1497
1498			totalPrice += bookingFee;
1499
1500			if ( 0 < totalPrice ) {
1501				themeCheckout.showCreditCardFields();
1502			} else {
1503				themeCheckout.hideCreditCardFields();
1504			}
1505
1506			totalPrice += themeCheckout.currentDonationAmount;
1507			themeCheckout.order.donation = themeCheckout.roundDecimalValue( themeCheckout.currentDonationAmount );
1508
1509			if ( themeCheckout.currentDonationAmount ) {
1510				// Helps *ensure* card fields are visible if e.g. have chosen a free ticket with no promo codes, but then checked the donation box.
1511				themeCheckout.showCreditCardFields();
1512			}
1513
1514			if ( themeCheckout.order.discount ) {
1515				totalPrice -= themeCheckout.order.discount;
1516			}
1517
1518			$( '.booking_fee_total' ).text( bookingFee.toFixed( 2 ) );
1519
1520			// Maybe subtract the amount the gift card can cover from the overall total.
1521			if ( 'undefined' !== typeof themeCheckout.order.giftCardTotal ) {
1522
1523				// Before modifying totalPrice, check giftCardTotal against it; if it's > 0 at this point, it means a donation was added or a promo code removed since the gift card was applied. So we need to make sure payment method is gc_partial, not gc.
1524				if (
1525					0 !== themeCheckout.order.giftCardTotal &&
1526					(totalPrice - themeCheckout.order.giftCardTotal) > 0
1527				) {
1528					themeCheckout.paymentMethod = 'gc_partial';
1529				}
1530
1531				totalPrice -= themeCheckout.order.giftCardTotal;
1532			}
1533
1534			themeCheckout.updateOrderTotal( totalPrice );
1535			themeCheckout.order.originalTotal = themeCheckout.roundDecimalValue( totalPrice );
1536			themeCheckout.order.bookingFee    = themeCheckout.roundDecimalValue( bookingFee );
1537
1538			$( '.total_purchase_price' ).text( themeCheckout.moneyFormat( totalPrice ) );
1539		},
1540		/**
1541		 * Applies gift card to order total.
1542		 *
1543		 * @return {bool} False on error.
1544		 */
1545		applyGiftCard: function () {
1546
1547			if ( !(themeCheckout.orderHasTickets() || themeCheckout.orderHasPaidTickets()) || themeCheckout.order.total === 0 ) {
1548				themeCheckout.removeGiftCard();
1549				themeCheckout.$giftCardError.html( 'Gift Cards not valid for this order' ).css( 'visibility', 'visible' );
1550				themeCheckout.$checkout.trigger( 'checkout-stop-loading' );
1551				return;
1552			}
1553
1554			if ( '' === themeCheckout.$giftCard.val() ) {
1555				themeCheckout.removeGiftCard();
1556				themeCheckout.$checkout.trigger( 'checkout-stop-loading' );
1557				return;
1558			}
1559
1560			themeCheckout.$checkout.trigger( 'checkout-start-loading' );
1561
1562			themeCheckout.$giftCardRow.addClass( 'gc-applying' );
1563
1564			themeCheckout.$giftCardApply.html( 'CHECKING...' );
1565
1566			var data = {
1567				action: 'filmbot_apply_gift_card',
1568				_ajax_nonce: $( document.getElementById( 'filmbot_checkout_nonce' ) ).val(),
1569				pm: themeCheckout.paymentMethod,
1570				order: $.extend( {}, themeCheckout.order, { email: themeCheckout.$emailInput.val() } ),
1571				gcAccount: themeCheckout.$giftCard.val(),
1572				gcCvv: themeCheckout.$giftCardCvv.val()
1573			};
1574
1575			$.ajax( {
1576				url: window.ajaxurl || window.themeSettings.ajaxUrl,
1577				method: 'POST',
1578				timeout: 60000,
1579				data: data,
1580				success: function ( response ) {
1581					if ( response.success && response.balanceAmount > 0.00 ) {
1582						themeCheckout.updateGiftCardAmounts( response.balanceAmount, response.differenceAmount );
1583						themeCheckout.$giftCardApply.html( 'REMOVE' );
1584						themeCheckout.$giftCard.prop( 'readonly', true );
1585						themeCheckout.$giftCardWrap.addClass( 'gc-code-accepted' );
1586					} else {
1587						// some error...
1588						var error = response.data || response.error || 'Unknown Error';
1589
1590						themeCheckout.triggerSentryWithExtras( '[ Apply Gift Card error ] ' + error, {
1591							orderId: themeCheckout.order.order_id || null,
1592							gcAccount: themeCheckout.$giftCard.val(),
1593							response: response
1594						} );
1595
1596						themeCheckout.$giftCardError.html( error ).css( 'visibility', 'visible' );
1597						themeCheckout.makeGiftCardEditable();
1598					}
1599
1600					themeCheckout.$checkout.trigger( 'checkout-stop-loading' );
1601				},
1602				error: function ( xhr, status, error ) {
1603					// timeout or server error on our end...
1604					themeCheckout.triggerSentryWithExtras( '[ Apply Gift Card error ] ' + error, {
1605						xhr: xhr,
1606						status: status,
1607						orderId: themeCheckout.order.order_id || null
1608					} );
1609
1610					themeCheckout.$giftCardError.html( 'An error occurred, please try again' ).css( 'visibility', 'visible' );
1611					themeCheckout.makeGiftCardEditable();
1612
1613					themeCheckout.$checkout.trigger( 'checkout-stop-loading' );
1614				}
1615			} ).done( function () {
1616				themeCheckout.$giftCardRow.removeClass( 'gc-applying' );
1617				themeCheckout.$checkout.trigger( 'checkout-stop-loading' );
1618			} );
1619		},
1620		/**
1621		 * If a gift card's already entered, in some cases we need to remove then reapply that gift card's info so the order totals--especially the portion the gift card will cover--are correct.
1622		 *
1623		 * @returns {void}
1624		 */
1625		maybeReapplyGiftCard: function () {
1626
1627			if ( !themeCheckout.$giftCard.length ) {
1628				return;
1629			}
1630			const giftCardAccount = themeCheckout.$giftCard.val();
1631			const giftCardCvv     = themeCheckout.$giftCardCvv.val();
1632
1633			if ( 'undefined' === typeof giftCardAccount ) {
1634				return;
1635			}
1636
1637			if ( '' === giftCardAccount ) {
1638				return;
1639			}
1640
1641			if ( themeCheckout.$giftCardCvv.length ) {
1642				if ( 'undefined' === typeof giftCardCvv ) {
1643					return;
1644				}
1645
1646				if ( '' === giftCardCvv ) {
1647					return;
1648				}
1649			}
1650
1651			themeCheckout.$checkout.trigger( 'checkout-start-loading' );
1652
1653			themeCheckout.removeGiftCard();
1654
1655			// Restore the gift card's account and CVV values so we can reapply the gift card.
1656			themeCheckout.$giftCard.val( giftCardAccount );
1657			if ( themeCheckout.$giftCardCvv.length ) {
1658				themeCheckout.$giftCardCvv.val( giftCardCvv );
1659			}
1660
1661			themeCheckout.applyGiftCard();
1662		},
1663		/**
1664		 * Preserves gift card field values, but makes them editable again; a useful state to return to after Gift Card ajax errors, for example.
1665		 */
1666		makeGiftCardEditable: function () {
1667			themeCheckout.$giftCard.prop( 'readonly', false );
1668			themeCheckout.$giftCardCvv.prop( 'readonly', false );
1669			themeCheckout.$giftCardApply.html( 'APPLY' );
1670			themeCheckout.$giftCardRow.removeClass( 'gc-applying' );
1671			themeCheckout.$giftCardWrap.removeClass( 'gc-code-accepted' );
1672		},
1673		/**
1674		 * Clears gift card fields.
1675		 */
1676		removeGiftCard: function () {
1677			themeCheckout.$giftCard.prop( 'readonly', false ).val( '' );
1678			themeCheckout.$giftCardCvv.prop( 'readonly', false ).val( '' );
1679			themeCheckout.$giftCardApply.html( 'APPLY' );
1680			themeCheckout.$giftCardWrap.removeClass( 'gc-code-accepted' );
1681
1682			themeCheckout.updateGiftCardAmounts( 0, themeCheckout.order.originalTotal );
1683			themeCheckout.paymentMethod = 'cc';
1684		},
1685		/**
1686		 * Updates checkout fields to incorporate gift card.
1687		 *
1688		 * @param {float} balanceAmount Gift card balance.
1689		 * @param {float} differenceAmount Difference between the order total and gift card balance, 0 when gift card can cover whole order.
1690		 */
1691		updateGiftCardAmounts: function ( balanceAmount, differenceAmount ) {
1692			balanceAmount    = parseFloat( balanceAmount, 10 );
1693			differenceAmount = parseFloat( differenceAmount, 10 );
1694
1695			// The gift card can pay for the whole order.
1696			if ( 0 === differenceAmount ) {
1697				themeCheckout.order.giftCardTotal = themeCheckout.roundDecimalValue( themeCheckout.order.total );
1698				themeCheckout.updateOrderTotal( 0 );
1699				themeCheckout.paymentMethod = 'gc';
1700
1701				themeCheckout.$giftCardTotal.find( '.gift_card_amount' ).text( themeCheckout.order.giftCardTotal.toFixed( 2 ) );
1702
1703				$( '.total_purchase_price' ).text( themeCheckout.moneyFormat( themeCheckout.order.total ) );
1704			} else {
1705				themeCheckout.order.giftCardTotal = themeCheckout.roundDecimalValue( balanceAmount );
1706				themeCheckout.updateOrderTotal( differenceAmount );
1707				themeCheckout.paymentMethod = 'gc_partial';
1708
1709				themeCheckout.$giftCardTotal.find( '.gift_card_amount' ).text( themeCheckout.order.giftCardTotal.toFixed( 2 ) );
1710
1711				$( '.total_purchase_price' ).text( themeCheckout.moneyFormat( themeCheckout.order.total ) );
1712			}
1713
1714			if ( jQuery.isEmptyObject( themeCheckout.$seatsData ) ) {
1715				themeCheckout.updateQuantity();
1716			} else {
1717				themeCheckout.updateQuantityBySeats();
1718			}
1719
1720			if ( balanceAmount > 0 ) {
1721				themeCheckout.$giftCardTotal.show();
1722			} else {
1723				themeCheckout.$giftCardTotal.hide();
1724			}
1725
1726			themeCheckout.maybeToggleCreditCardFields();
1727		},
1728		applyDiscountCode: function ( params ) {
1729			if ( !(themeCheckout.orderHasTickets() || themeCheckout.orderHasPaidTickets()) ) {
1730				themeCheckout.removeDiscountCode();
1731				themeCheckout.$discountCodeError.html( 'Discounts not valid for this order' ).css( 'visibility', 'visible' );
1732				return false;
1733			}
1734
1735			if ( themeCheckout.$discountCode.val() !== '' ) {
1736
1737				themeCheckout.applyingDiscountCode = true;
1738
1739				themeCheckout.$checkout.trigger( 'checkout-start-loading' );
1740
1741				themeCheckout.$discountCodeApply.html( 'CHECKING...' );
1742
1743				var data = {
1744					action: 'filmbot_apply_discount_code',
1745					_ajax_nonce: $( '#filmbot_checkout_nonce' ).val(),
1746					pm: themeCheckout.paymentMethod,
1747					address: {
1748						first_name: $( '#first_name' ).val(),
1749						last_name: $( '#last_name' ).val(),
1750						billing_address: $( '#billing_address' ).val(),
1751						country: $( '#country' ).val(),
1752						postal_code: $( '#postal_code' ).val(),
1753						dnd: themeCheckout.$dndCheckbox.is( ':checked' )
1754					},
1755					order: $.extend( {}, themeCheckout.order, { email: themeCheckout.$emailInput.val() } ),
1756					discount_code: themeCheckout.$discountCode.val()
1757				};
1758
1759				themeCheckout.$discountCodeError.html( '' ).css( 'visibility', 'hidden' );
1760				themeCheckout.$discountCodeError.parent().removeClass( 'large-error-message' );
1761
1762				if ( typeof $.applyFilter !== 'undefined' ) {
1763					data = $.applyFilter( 'filmbot-modify-discount-data', data );
1764				}
1765
1766				$.ajax( {
1767					url: window.ajaxurl || window.themeSettings.ajaxUrl,
1768					method: 'POST',
1769					timeout: 60000,
1770					data: data,
1771					success: function ( response ) {
1772						if ( response.success && response.discountAmount >= 0 ) {
1773							themeCheckout.updateDiscounts( response.discountAmount );
1774							themeCheckout.$discountCodeApply.html( 'REMOVE' );
1775							themeCheckout.$discountCode.prop( 'readonly', true ).addClass( 'discount-code--applied' );
1776							themeCheckout.maybeReapplyGiftCard();
1777
1778							if ( params && params.successMessage && themeCheckout.$discountCodeSuccess.length ) {
1779								themeCheckout.$discountCodeSuccess.html( params.successMessage ).css( 'visibility', 'visible' );
1780							}
1781
1782							if ( params?.isMembershipLookup ) {
1783								themeCheckout.appliedCodeForEmail = themeCheckout.$emailInput.val();
1784							}
1785
1786							window.themeUx.$document.trigger( 'filmbot-discount-code-applied', response );
1787						} else {
1788							// some error...
1789							var error = !!response.error ? response.error.toString() : 'Unknown Error';
1790							themeCheckout.$discountCodeError.html( error ).css( 'visibility', 'visible' );
1791							if ( error.length > 65 ) {
1792								themeCheckout.$discountCodeError.parent().addClass( 'large-error-message' );
1793							}
1794							if ( params?.isMembershipLookup || themeCheckout.appliedCodeForEmail ) {
1795								themeCheckout.membershipCodeRemovedForEmail = themeCheckout.$emailInput.val();
1796							}
1797							themeCheckout.removeDiscountCode();
1798
1799							themeCheckout.triggerSentryWithExtras( '[ Checkout error ] ' + error, {
1800								response: response,
1801							} );
1802						}
1803
1804						themeCheckout.$checkout.trigger( 'checkout-stop-loading' );
1805					},
1806					error: function ( xhr, status, error ) {
1807						// timeout or server error on our end...
1808						themeCheckout.triggerSentryWithExtras( '[ Apply Discount Code error ] ' + error, {
1809							xhr: xhr,
1810							status: status,
1811							orderId: themeCheckout.order.order_id || null
1812						} );
1813
1814						themeCheckout.$discountCodeError.html( 'An error occurred, please try again' ).css( 'visibility', 'visible' );
1815						themeCheckout.removeDiscountCode();
1816						themeCheckout.$checkout.trigger( 'checkout-stop-loading' );
1817					},
1818					complete: function () {
1819						themeCheckout.applyingDiscountCode = false;
1820					}
1821				} );
1822
1823				return false;
1824			}
1825		},
1826		removeDiscountCode: function () {
1827			themeCheckout.$checkout.trigger( 'checkout-start-loading' );
1828
1829			themeCheckout.$discountCode.prop( 'readonly', false ).removeClass( 'discount-code--applied' );
1830			themeCheckout.$discountCode.val( '' );
1831			themeCheckout.updateDiscounts( '0' );
1832			themeCheckout.$discountCodeApply.html( 'APPLY' );
1833
1834			themeCheckout.$checkout.trigger( 'checkout-stop-loading' );
1835
1836			themeCheckout.appliedCodeForEmail = '';
1837			themeCheckout.$discountCodeSuccess.css( 'visibility', 'hidden' );
1838
1839			window.themeUx.$document.trigger( 'filmbot-discount-code-removed' );
1840
1841			return false;
1842		},
1843		/**
1844		 * Updates order total both locally (in the Totals section of the checkout)
1845		 * and in Payment Request API if applicable (in the payment method request popup).
1846		 *
1847		 * @param newTotal
1848		 */
1849		updateOrderTotal: function ( newTotal ) {
1850
1851			newTotal = isNaN( newTotal ) ? 0 : parseFloat( newTotal );
1852
1853			themeCheckout.order.total = themeCheckout.roundDecimalValue( newTotal );
1854
1855			// Do not update Payment Request if it is not initialized.
1856			if ( !themeCheckout.paymentRequest ) {
1857				return;
1858			}
1859
1860			// Update total in Payment Request popup.
1861			themeCheckout.paymentRequest.update( {
1862				total: {
1863					label: 'Total',
1864					amount: Math.round( Math.max( newTotal, 0 ) * 100 )
1865				}
1866			} );
1867		},
1868		updateDiscounts: function ( discountAmount ) {
1869
1870			discountAmount               = parseFloat( discountAmount, 10 );
1871			themeCheckout.order.discount = 0;
1872
1873			if ( jQuery.isEmptyObject( themeCheckout.$seatsData ) ) {
1874				themeCheckout.updateQuantity();
1875			} else {
1876				themeCheckout.updateQuantityBySeats();
1877			}
1878
1879			themeCheckout.updateOrderTotal( themeCheckout.order.total - discountAmount );
1880
1881			if ( themeCheckout.order.total < 0 ) {
1882				themeCheckout.updateOrderTotal( 0 );
1883			}
1884
1885			themeCheckout.order.discount = themeCheckout.roundDecimalValue( discountAmount );
1886
1887			themeCheckout.$discount.find( '.discount_amount' ).text( discountAmount.toFixed( 2 ) );
1888
1889			$( '.total_purchase_price' ).text( themeCheckout.moneyFormat( themeCheckout.order.total ) );
1890
1891			if ( discountAmount > 0 ) {
1892				themeCheckout.$discount.show();
1893			} else {
1894				themeCheckout.$discount.hide();
1895			}
1896
1897			if ( themeCheckout.orderIsFreeCheckout() ) {
1898				themeCheckout.hideCreditCardFields();
1899
1900				// If a promo code has been entered that equals the cost of the order, remove the gift card and just use the 100% promo code.
1901				if ( 'undefined' !== typeof themeCheckout.order.giftCardTotal ) {
1902					if ( themeCheckout.order.discount >= (themeCheckout.order.originalTotal + themeCheckout.order.giftCardTotal) ) {
1903						themeCheckout.removeGiftCard();
1904						$( '.gift-card-row' ).slideUp();
1905					}
1906				} else {
1907					// If a gift card hasn't been entered yet, still hide the gift card fields in the case of a 100% promo.
1908					if ( themeCheckout.order.discount >= themeCheckout.order.originalTotal ) {
1909						themeCheckout.removeGiftCard();
1910						$( '.gift-card-row' ).slideUp();
1911					}
1912				}
1913			} else {
1914				themeCheckout.showCreditCardFields();
1915				$( '.gift-card-row' ).slideDown();
1916			}
1917		},
1918		/**
1919		 * Whether the selected Card payment method is a manual credit card payment
1920		 * (otherwise it may be an Apple Pay or Google Pay payment etc.).
1921		 *
1922		 * @returns {boolean}
1923		 */
1924		isManualCreditCardPayment: function () {
1925			return themeCheckout.ccMethod === 'manual-cc';
1926		},
1927		showCreditCardFields: function () {
1928
1929			// Show Credit Card fields only for regular credit card payments
1930			// and hide them for other payment methods like Apple Pay or Google Pay.
1931			if ( themeCheckout.isManualCreditCardPayment() ) {
1932				themeCheckout.$ccFields.slideDown();
1933			}
1934
1935			// Show Payment Method selector if we have additional
1936			// Payment Methods like Apple Pay or Google Pay
1937			if ( themeCheckout.hasAdditionalPMs ) {
1938				themeCheckout.$pmSelector.slideDown();
1939			}
1940
1941			$( '.checkout-container' ).removeClass( 'checkout--free' );
1942		},
1943		hideCreditCardFields: function () {
1944			themeCheckout.$ccFields.slideUp();
1945
1946			// Hide Payment Method selector if we have additional Payment Methods
1947			if ( themeCheckout.hasAdditionalPMs ) {
1948				themeCheckout.$pmSelector.slideUp();
1949			}
1950
1951			$( '.checkout-container' ).addClass( 'checkout--free' );
1952
1953			themeCheckout.selectPaymentMethodType();
1954		},
1955		applyMembershipCosts: function () {
1956			var currentPlanInfo = themeCheckout.getCurrentMembershipPlanInfo();
1957			if ( !currentPlanInfo['id'] ) {
1958				return;
1959			}
1960
1961			// Set the plan size based on the selected radio button.
1962			var currentSize = window.themeMembership.$planSizeToggle.filter( ':checked' ).val() || 'single';
1963
1964			themeCheckout.order.membership      = themeCheckout.roundDecimalValue( currentPlanInfo['costs'][currentSize] );
1965			themeCheckout.order.membership_plan = {
1966				'title': currentPlanInfo['title'],
1967				'id': currentPlanInfo['id'],
1968				'size': currentSize
1969			};
1970
1971			themeCheckout.updateOrderTotal( themeCheckout.order.membership );
1972			themeCheckout.maybeToggleCreditCardFields();
1973		},
1974		setMembershipRenewalDetails: function () {
1975			// Skip if the membership cost JSON isn't available (membership renewal is already paid).
1976			var currentPlanInfo = themeCheckout.getCurrentMembershipPlanInfo();
1977			if ( !currentPlanInfo['id'] ) {
1978				return;
1979			}
1980			// On the renewal page, planType is added to the membership cost JSON directly.
1981			const planType = currentPlanInfo.planType || $( '#membership_plan_selected_size' ).val();
1982
1983			const currentPlanCosts = themeCheckout.getCurrentMembershipPlanCosts();
1984
1985			themeCheckout.order.membership = themeCheckout.roundDecimalValue( parseFloat( currentPlanCosts[planType] ) );
1986
1987			themeCheckout.order.membership_renewal_id = document.getElementById( 'membership-renewal-id' ).value;
1988
1989			themeCheckout.order.membership_plan = {
1990				'title': currentPlanInfo['title'],
1991				'id': currentPlanInfo['id'],
1992				'size': planType
1993			};
1994
1995			themeCheckout.updateOrderTotal( themeCheckout.order.membership );
1996			themeCheckout.maybeToggleCreditCardFields();
1997		},
1998		applyTicketPackCosts: function () {
1999			var ticketPackCost = themeCheckout.$ticketPackValue['cost'];
2000
2001			themeCheckout.order.ticket_pack = {
2002				'title': themeCheckout.$ticketPackValue['offer'],
2003				'id': themeCheckout.$ticketPackValue['id'],
2004				'cost': ticketPackCost
2005			};
2006
2007			themeCheckout.updateOrderTotal( ticketPackCost );
2008			themeCheckout.maybeToggleCreditCardFields();
2009		},
2010		validateInputs: function ( $item ) {
2011			if ( $item.attr( 'type' ) == 'radio' ) {
2012				let $items = $( '[name=' + $item.attr( 'name' ) + ']' )
2013				$item.closest( '.radio-group' ).toggleClass( 'error', $items.filter( ':checked' ).length === 0 );
2014				return;
2015			}
2016			var val = $item.val();
2017
2018			if ( $item.hasClass( 'required' ) ) {
2019				$item.toggleClass( 'error', $.trim( val ) === '' || val === null );
2020			}
2021		},
2022		validateTermsCheckbox: function () {
2023			var checked = $( 'input[name=terms]' ).prop( 'checked' );
2024			themeCheckout.$terms.toggleClass( 'error', checked !== true );
2025		},
2026		validateStandaloneDonationAmount: function () {
2027			if ( $( 'body' ).hasClass( 'page-template-donate-template' ) ) {
2028				var donationAmount = parseFloat( themeCheckout.$donationAmount.val() || 0 );
2029				themeCheckout.$donationAmount.parent().toggleClass( 'error', donationAmount === 0 );
2030				const message = donationAmount === 0 ? 'Please enter the donation amount' : '';
2031				if ( message ) {
2032					themeCheckout.setSubmitError( message );
2033				} else {
2034					themeCheckout.$submitError.text( '' );
2035				}
2036			}
2037		},
2038		isValidEmailAddress: function ( emailAddress ) {
2039			var pattern = new RegExp( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ );
2040			return pattern.test( emailAddress );
2041		},
2042		checkEmailInput: function () {
2043			var emailAddress = themeCheckout.$emailInput.val();
2044			if ( !themeCheckout.isValidEmailAddress( emailAddress ) ) {
2045				themeCheckout.$emailInput.addClass( 'error' );
2046			} else {
2047				themeCheckout.$emailInput.removeClass( 'error' );
2048			}
2049			return themeCheckout.$emailInput.hasClass( 'error' );
2050		},
2051		isValidPhoneNumber: function ( phoneNumber ) {
2052			var pattern = /\d/g;
2053			return phoneNumber.match( pattern ) !== null && phoneNumber.match( pattern ).length === 10;
2054		},
2055		validatePhoneNumber: function () {
2056			if ( !themeCheckout.$dndCheckbox.is( ':checked' ) )
2057				return;
2058			var phoneNumber = themeCheckout.$phone.val();
2059			if ( !themeCheckout.isValidPhoneNumber( phoneNumber ) ) {
2060				themeCheckout.$phone.addClass( 'error' );
2061			}
2062		},
2063		isValidPercent: function ( percent ) {
2064			var pattern = /\d/g;
2065			return percent.match( pattern ) !== null && percent.match( pattern ).length < 3;
2066		},
2067		validateTipPercent: function () {
2068			if ( !themeCheckout.$dndCheckbox.is( ':checked' ) )
2069				return;
2070			var percent = themeCheckout.$tipPercent.val();
2071			if ( !themeCheckout.isValidPercent( percent ) ) {
2072				themeCheckout.$tipPercent.addClass( 'error' );
2073				themeCheckout.$dndStep2.find( '.error-message' ).css( 'visibility', 'visible' );
2074			} else {
2075				themeCheckout.$tipPercent.removeClass( 'error' );
2076				themeCheckout.$dndStep2.find( '.error-message' ).css( 'visibility', 'hidden' );
2077			}
2078		},
2079		validateDonationAmount: async function ( paymentMethodData ) {
2080			return new Promise( function ( resolve, reject ) {
2081				if ( themeCheckout.currentDonationAmount && themeCheckout.currentDonationAmount >= themeCheckout.maxUnconfirmedDonationAmount ) {
2082					window.Swal.fire(
2083						{
2084							title: "Confirm donation amount",
2085							text: "You've chosen to donate $" + themeCheckout.moneyFormat( themeCheckout.currentDonationAmount ) + ". Ok to continue?",
2086							showCancelButton: true,
2087							confirmButtonText: "Yes",
2088							cancelButtonText: "No",
2089						}
2090					).then( ( result ) => {
2091						result.value ? resolve( true ) : resolve( false );
2092					} );
2093				} else {
2094					resolve( true );
2095				}
2096			} );
2097		},
2098		validateForm: async function ( e ) {
2099
2100			e.preventDefault();
2101
2102			themeCheckout.$fields.each( function () {
2103				themeCheckout.validateInputs( $( this ) );
2104			} );
2105
2106			themeCheckout.checkEmailInput();
2107
2108			if ( themeCheckout.$terms.length ) {
2109				themeCheckout.validateTermsCheckbox();
2110			}
2111
2112			themeCheckout.validateStandaloneDonationAmount();
2113
2114			if ( $( '.error' ).length ) {
2115				return false;
2116			}
2117
2118			var isDonationValid = await themeCheckout.validateDonationAmount();
2119			if ( !isDonationValid ) {
2120				return false;
2121			}
2122
2123			themeCheckout.$checkout.find( '*' ).filter( '.payment-input-container.active *' ).blur();
2124			themeCheckout.$fields.blur();
2125
2126			// if it's a free or cash order, skip the payment method creation/retrieving and submit the checkout
2127			// if the payment method is gift card, we just submit
2128			// credit cards are handled separately by the payment method creation/retrieving callback
2129			if ( themeCheckout.orderIsFreeCheckout() || themeCheckout.paymentMethod === 'cash' ) {
2130				themeCheckout.submitCheckout();
2131			} else if ( themeCheckout.paymentMethod === 'gc' ) {
2132				themeCheckout.submitCheckout();
2133			} else if ( themeCheckout.paymentMethod === 'cc' || themeCheckout.paymentMethod === 'gc_partial' ) {
2134				if ( themeCheckout.isManualCreditCardPayment() ) {
2135
2136					// Request payment method and then initialize purchase if regular credit card payment
2137					themeCheckout.createCardPaymentMethod();
2138				} else {
2139
2140					// Request payment method and then initialize purchase for alternative payment methods (Apple Pay, Google Pay)
2141					themeCheckout.requestExistingPaymentMethod();
2142				}
2143			}
2144			return true;
2145		},
2146		disableForm: function ( buttonLabel ) {
2147
2148			// this completely disabled the form to prevent multiple submissions
2149			themeCheckout.$checkoutContainer.addClass( 'processing-payment' );
2150
2151			themeCheckout.$submitButton.prop( 'disabled', true ).prop( 'value', !buttonLabel ? 'PLEASE WAIT...' : buttonLabel );
2152
2153			themeCheckout.$form.get( 0 ).onsubmit = function ( e ) {
2154				e.preventDefault();
2155				e.stopPropagation();
2156				return false;
2157			};
2158
2159			themeCheckout.$submitError.html( '' );
2160		},
2161
2162		/**
2163		 * Enables all checkout form inputs, possibly accompanied by an error message.
2164		 *
2165		 * @param {mixed} $message String (single message) or array/object of strings (multiple messages). Pass an empty object to bypass fallback value of "Unknown Error".
2166		 */
2167		setSubmitError: function ( message ) {
2168
2169			message = {
2170				'Failed to create token': 'Please ensure your credit card number is entered correctly.'
2171			}[message] || message || 'Unknown Error';
2172
2173			themeCheckout.$submitError.attr( 'role', 'alert' );
2174
2175			switch ( typeof message ) {
2176				case 'object':
2177					if ( message.length === 1 ) {
2178						themeCheckout.$submitError.text( message[0] );
2179						break;
2180					}
2181
2182					var messages = `<ul>`;
2183
2184					$.each( message, function ( i, msg ) {
2185						messages += `<li>${msg}</li>`;
2186					} );
2187
2188					messages += `</ul>`;
2189
2190					themeCheckout.$submitError.html( messages );
2191					break;
2192				case 'string':
2193				default:
2194					themeCheckout.$submitError.text( message );
2195					break;
2196			}
2197		},
2198		/**
2199		 * Enables all checkout form inputs, possibly accompanied by an error message.
2200		 *
2201		 * @param {mixed} $message String (single message) or array/object of strings (multiple messages). Pass an empty object to bypass fallback value of "Unknown Error".
2202		 * @param {boolean} replaceErrorMessage Whether to replace the current error message with the provided one. Set to false to preserve the current error message.
2203		 */
2204		enableForm: function ( message, replaceErrorMessage = true ) {
2205
2206			// this re-enables the form in the case of a card declined or other submission issue
2207			themeCheckout.$checkoutContainer.removeClass( 'processing-payment' );
2208
2209			if ( replaceErrorMessage || themeCheckout.$submitError.text() === '' ) {
2210				themeCheckout.setSubmitError( message );
2211			}
2212
2213			themeCheckout.$form.get( 0 ).onsubmit = function ( e ) {};
2214
2215			var buttonText = 'PURCHASE';
2216			if ( window.themeUx.$body.hasClass( 'page-template-donate-template' ) ) {
2217				buttonText = 'DONATE';
2218			} else if ( window.themeUx.$body.hasClass( 'membership-renew' ) ) {
2219				buttonText = 'RENEW';
2220			}
2221
2222			themeCheckout.$submitButton.prop( 'disabled', false ).prop( 'value', buttonText );
2223		},
2224
2225		/**
2226		 * Displays available additional payment methods in the UI.
2227		 *
2228		 * @param enabledMethods
2229		 */
2230		displayAvailablePaymentMethods: function ( enabledMethods ) {
2231
2232			// Do nothing if no config provided
2233			if ( !enabledMethods ) {
2234				return;
2235			}
2236
2237			// Show Payment Method selector
2238			themeCheckout.$pmSelector.removeClass( 'hidden' );
2239			if ( enabledMethods.applePay ) {
2240
2241				// Show Apple Pay option
2242				themeCheckout.$pmSelector.find( '.apple-pay' ).removeClass( 'hidden' );
2243			}
2244			if ( enabledMethods.googlePay ) {
2245
2246				// Show Google Pay option
2247				themeCheckout.$pmSelector.find( '.google-pay' ).removeClass( 'hidden' );
2248			}
2249
2250			// Define if we have additional Payment Methods like Apple Pay or Google Pay based on the enabledMethods.
2251			themeCheckout.hasAdditionalPMs = enabledMethods && (enabledMethods.googlePay || enabledMethods.applePay);
2252			if ( themeCheckout.hasAdditionalPMs && !themeCheckout.orderIsFreeCheckout() ) {
2253
2254				// Display Payment Method selector only if we have additional Payment Methods
2255				themeCheckout.$pmSelector.slideDown();
2256			}
2257		},
2258
2259		/**
2260		 * Handles selection of payment method type in the UI.
2261		 *
2262		 * @param $paymentMethodItem
2263		 */
2264		selectPaymentMethodType: function ( $paymentMethodItem ) {
2265
2266			// Determine whether to change credit card fields visibility
2267			// (depending on whether a specific payment method item is provided)
2268			var shouldChangeCCFields = !!$paymentMethodItem;
2269
2270			// If no specific payment method item provided, select the first one (default credit card payment method)
2271			$paymentMethodItem = $paymentMethodItem || themeCheckout.$pmSelectorItem.first();
2272
2273			// Check selected payment method type
2274			themeCheckout.ccMethod = $paymentMethodItem.data( 'pm-type' );
2275
2276			// Set current payment method as selected
2277			themeCheckout.$pmSelectorItem.removeClass( 'selected' );
2278			$paymentMethodItem.addClass( 'selected' );
2279
2280			// Hide or show credit card fields based on selected payment method
2281			if ( $paymentMethodItem.hasClass( 'credit-card' ) ) {
2282				if ( shouldChangeCCFields ) {
2283					themeCheckout.$ccFields.slideDown();
2284
2285					// Hide select that allows to choose Cash as payment method
2286					if ( themeCheckout.$pmMethodSelect ) {
2287						themeCheckout.$pmMethodSelect.slideDown();
2288					}
2289				}
2290			} else {
2291				if ( shouldChangeCCFields ) {
2292					themeCheckout.$ccFields.slideUp();
2293
2294					// Show select that allows to choose Cash as payment method
2295					if ( themeCheckout.$pmMethodSelect ) {
2296						themeCheckout.$pmMethodSelect.slideUp();
2297					}
2298				}
2299			}
2300		},
2301
2302		/**
2303		 * Initialize Stripe Elements for displaying the credit card form.
2304		 */
2305		initStripeElements: function () {
2306
2307			// Skip Stripe Elements initialization if there is no credit card fields.
2308			if ( !$( '#stripe-card-number' ).length ) {
2309				return;
2310			}
2311
2312			var color            = window.themeSettings.theme_mods.cc_text_color || window.themeSettings.theme_mods.page_background_color || '#FFF';
2313			var placeholderColor = window.themeSettings.theme_mods.cc_focused_labels || window.themeSettings.theme_mods.accent_color || '#FFF';
2314			var errorColor       = window.themeSettings.theme_mods.error_color || 'red';
2315
2316			const options        = STRIPE_CONNECTED_ACCOUNT_ID ? { stripeAccount: STRIPE_CONNECTED_ACCOUNT_ID } : {};
2317			themeCheckout.stripe = Stripe( STRIPE_PUBLIC_KEY, options );
2318
2319			themeCheckout.elements = themeCheckout.stripe.elements();
2320
2321			// Floating labels
2322			var inputs = $( '#credit-card-inputs .input' ).get();
2323
2324			Array.prototype.forEach.call( inputs, function ( input ) {
2325
2326				input.addEventListener( 'focus', function () {
2327					input.classList.add( 'focused' );
2328				} );
2329
2330				input.addEventListener( 'blur', function () {
2331					input.classList.remove( 'focused' );
2332				} );
2333
2334				input.addEventListener( 'keyup', function () {
2335					if ( input.value.length === 0 ) {
2336						input.classList.add( 'empty' );
2337					} else {
2338						input.classList.remove( 'empty' );
2339					}
2340				} );
2341			} );
2342
2343			var elementStyles = {
2344				base: {
2345					color: color, // valid values typed in
2346					fontWeight: 500,
2347					fontFamily: 'Verdana, Helvetica, Verdana, sans-serif',
2348					fontSize: '16px',
2349					fontSmoothing: 'antialiased',
2350					'::placeholder': {
2351						color: placeholderColor
2352					},
2353					':-webkit-autofill': {
2354						color: placeholderColor
2355					}
2356				},
2357				invalid: {
2358					color: errorColor,
2359					'::placeholder': {
2360						color: placeholderColor
2361					}
2362				}
2363			};
2364
2365			var elementClasses = {
2366				focus: 'focused',
2367				empty: 'empty',
2368				invalid: 'invalid'
2369			};
2370
2371			themeCheckout.cardNumber = themeCheckout.elements.create( 'cardNumber', {
2372				style: elementStyles,
2373				classes: elementClasses
2374			} );
2375
2376			themeCheckout.cardNumber.mount( '#stripe-card-number' );
2377
2378			themeCheckout.cardNumber.on( 'ready', function () {
2379				themeCheckout.stripeElementsInit = true;
2380			} );
2381
2382			var cardExpiry = themeCheckout.elements.create( 'cardExpiry', {
2383				style: elementStyles,
2384				classes: elementClasses
2385			} );
2386
2387			cardExpiry.mount( '#stripe-card-expiry' );
2388
2389			var cardCvc = themeCheckout.elements.create( 'cardCvc', {
2390				style: elementStyles,
2391				classes: elementClasses,
2392				placeholder: '',
2393			} );
2394
2395			cardCvc.mount( '#stripe-card-cvc' );
2396		},
2397
2398		/**
2399		 * Initializes Stripe Payment Request API.
2400		 */
2401		initPaymentRequest: function () {
2402
2403			// Skip Payment Request API initialization if Stripe failed to initialize.
2404			if ( !themeCheckout.stripe ) {
2405				return;
2406			}
2407
2408			// Skip extra Payment Methods initialization if there is no element for them.
2409			themeCheckout.paymentRequest = themeCheckout.stripe.paymentRequest( {
2410				country: window.themeSettings.country_code || 'US',
2411				currency: (window.themeSettings.currency_code || 'usd').toLowerCase(),
2412				total: {
2413					label: 'Total',
2414					amount: Math.round( themeCheckout.order.total * 100 ), // in cents
2415				},
2416			} );
2417
2418			// Check availability of the Payment Request API
2419			themeCheckout.paymentRequest.canMakePayment().then( function ( enabledMethods ) {
2420				themeCheckout.displayAvailablePaymentMethods( enabledMethods );
2421			}, function () {
2422				themeCheckout.$pmSelector.slideUp();
2423			} );
2424
2425			// Handle payment method selection.
2426			// This handler is triggered when the user selects a payment method in the payment request popup.
2427			themeCheckout.paymentRequest.on( 'paymentmethod', async ( ev ) => {
2428				themeCheckout.stripePaymentRequestEvent = ev;
2429
2430				// If the reservation has expired by the time the customer selects a payment method,
2431				// complete the Payment Method request to close the popup.
2432				if ( themeCheckout.hasReservationExpired ) {
2433					themeCheckout.completePaymentMethodRequest( false );
2434
2435					return;
2436				}
2437
2438				// Submit the checkout with the selected payment method.
2439				themeCheckout.cardPaymentMethod = ev.paymentMethod;
2440				themeCheckout.initializePurchase();
2441			} );
2442
2443			// Handle closing of the payment request UI
2444			// This handler is triggered when the user cancels the payment request (closes the popup)
2445			themeCheckout.paymentRequest.on( 'cancel', async ( ev ) => {
2446
2447				// Enable form for further user actions
2448				if ( !themeCheckout.checkoutSubmitted ) {
2449					themeCheckout.enableForm( 'Payment cancelled.', false );
2450				}
2451			} );
2452
2453		},
2454		initStripeElementsChecks: function () {
2455
2456			$( document ).on( 'focus', '#stripe-card-number', function ( e ) {
2457				if ( !themeCheckout.stripeElementsInit ) {
2458					themeCheckout.triggerStripeElementsInitError();
2459				}
2460			} );
2461
2462			$( document ).on( 'click', 'label[for="stripe-card-number"]', function ( e ) {
2463				if ( !themeCheckout.stripeElementsInit ) {
2464					themeCheckout.triggerStripeElementsInitError();
2465				}
2466			} );
2467		},
2468		triggerStripeElementsInitError: function () {
2469
2470			// Log a Sentry error if Stripe Elements failed to init.
2471			if ( 'undefined' !== typeof Sentry ) {
2472
2473				Sentry.withScope( function ( scope ) {
2474					scope.setLevel( 'error' );
2475					Sentry.captureMessage( '[ Checkout error ] Stripe Elements were not initialized' );
2476				} );
2477
2478				// Console error if Sentry not configured.
2479			} else {
2480				console.error( '[ Checkout error ] Stripe Elements were not initialized' );
2481			}
2482		},
2483		/**
2484		 * Requests a Stripe PaymentMethod object using the entered card details.
2485		 */
2486		createCardPaymentMethod: function () {
2487			if ( typeof STRIPE_PUBLIC_KEY === 'undefined' || !STRIPE_PUBLIC_KEY ) {
2488				return;
2489			}
2490
2491			// Create a PaymentMethod using the entered card details.
2492			themeCheckout.stripe.createPaymentMethod( {
2493				type: 'card',
2494				card: themeCheckout.cardNumber,
2495				billing_details: {
2496					name: $( '#first_name' ).val() + ' ' + $( '#last_name' ).val(),
2497					email: $( '#email' ).val(),
2498					address: {
2499						postal_code: $( '#postal_code' ).val()
2500					}
2501				}
2502			} ).then( function ( result ) {
2503				if ( typeof result.error !== 'undefined' && result.error ) {
2504
2505					themeCheckout.triggerSentryWithExtras( '[ Stripe payment method error ] ' + result.error.message, {
2506						error: result.error,
2507						orderId: themeCheckout.order.order_id || null
2508					} );
2509
2510					// If there was an error creating the PaymentMethod, re-enable the form and display the error message.
2511					themeCheckout.enableForm( result.error.message );
2512					window.themeUx.$document.trigger( 'checkout-error', {
2513						type: 'stripe-payment-method',
2514						details: result.error
2515					} );
2516				} else if ( !themeCheckout.checkoutSubmitted ) {
2517
2518					// Submit the checkout and pass the PaymentMethod.
2519					themeCheckout.cardPaymentMethod = result.paymentMethod;
2520					themeCheckout.initializePurchase();
2521				}
2522			} );
2523		},
2524
2525		/**
2526		 * Displays the Payment Method popup if it is initialized.
2527		 */
2528		requestExistingPaymentMethod: function () {
2529			if ( themeCheckout.paymentRequest ) {
2530				themeCheckout.paymentRequest.show();
2531			}
2532		},
2533
2534		/**
2535		 * Gathers reservations data from the tickets in the order.
2536		 */
2537		gatherReservationsFromTickets: function ( data ) {
2538			var reservations = [];
2539
2540			var tickets = [];
2541			$.each( Object.keys( data.order.tickets ), function ( i, ticket_type ) {
2542				var quantity = data.order.tickets[ticket_type];
2543				for ( var k = 0; k < quantity; k++ ) {
2544					tickets.push( { ticket_type } );
2545				}
2546			} );
2547
2548			reservations.push( {
2549				showtime_id: data.order.showtime,
2550				requested_tickets: tickets
2551			} );
2552
2553			return reservations;
2554		},
2555
2556		/**
2557		 * Completes the Payment Method request, if any is active.
2558		 * This method closes the Payment Method popup with provided status.
2559		 *
2560		 * @param success
2561		 */
2562		completePaymentMethodRequest: function ( success ) {
2563
2564			// Do nothing if there is no active Payment Method request.
2565			if ( !themeCheckout.stripePaymentRequestEvent ) {
2566				return;
2567			}
2568
2569			// Complete the Payment Method request
2570			// and close the popup with payment method selection.
2571			// In the donation flow with hCaptcha,
2572			// we need to close the Payment Method popup to re-validate the hCaptcha.
2573			if ( success || $( '[name="h-captcha-response"]' ).length > 0 ) {
2574				themeCheckout.stripePaymentRequestEvent.complete( 'success' );
2575			} else {
2576				themeCheckout.stripePaymentRequestEvent.complete( 'fail' );
2577			}
2578
2579			// Clear the active Payment Method request to prepare for future use.
2580			themeCheckout.stripePaymentRequestEvent = null;
2581		},
2582
2583		/**
2584		 * Gathers all necessary data from the checkout form to create/initialize a purchase.
2585		 *
2586		 * @returns object
2587		 */
2588		getData: function () {
2589			var data = {
2590				action: 'filmbot_create_purchase', // default action to create purchase
2591				_ajax_nonce: $( '#filmbot_checkout_nonce' ).val(),
2592				pm: themeCheckout.paymentMethod,
2593				address: {
2594					first_name: $( '#first_name' ).val(),
2595					last_name: $( '#last_name' ).val(),
2596					postal_code: $( '#postal_code' ).val()
2597				},
2598				order: $.extend( {}, themeCheckout.order, { email: themeCheckout.$emailInput.val() } ),
2599				discount_code: themeCheckout.$discountCode.val()
2600			};
2601
2602			$.each( ['mailing_address', 'mailing_address_2', 'mailing_city', 'mailing_state', 'mailing_country', 'mailing_postal_code'], function ( i, param ) {
2603				var elem = $( '#' + param );
2604				if ( elem.length && elem.val() ) {
2605					data.address[param] = elem.val();
2606				}
2607			} );
2608
2609			if ( themeCheckout.$membershipSignup.length ) {
2610				data = window.themeMembership.getMemberData( data );
2611			}
2612
2613			if ( $( 'body' ).is( '.membership-renew' ) ) {
2614				data                     = window.themeMembership.getMemberData( data );
2615				data                     = window.themeMembershipUpdate.getData( data );
2616				data.member_1.first_name = $( '#first_name' ).val();
2617				data.member_1.last_name  = $( '#last_name' ).val();
2618				data.member_1.email      = $( '#email' ).val();
2619				data._ajax_nonce         = $( '#filmbot_checkout_nonce' ).val();
2620
2621				if ( themeCheckout.paymentMethod === 'cash' ) {
2622					data.action = 'filmbot_create_purchase';
2623				}
2624			}
2625
2626			if ( themeCheckout.$seatsData && !jQuery.isEmptyObject( themeCheckout.$seatsData ) ) {
2627
2628				// For reserved tickets, we need to set the action to initialize or finalize purchase accordingly
2629				// depending on whether the order total is zero or not.
2630				// If it is empty then we need to finalize the purchase.
2631				data.action   = data.order.total ? 'filmbot_initialize_purchase' : 'filmbot_finalize_purchase';
2632				data.order_id = themeCheckout.$reservedOrderId;
2633
2634			} else if ( !data.order.reservations && data.order.tickets ) {
2635
2636				// Add reservations in the proper format in case they are not already provided
2637				data.order.reservations = themeCheckout.gatherReservationsFromTickets( data );
2638			}
2639
2640			if ( themeCheckout.$donationAmount.length ) {
2641				data.address.mailing_address     = $( '#mailing_address' ).val();
2642				data.address.mailing_address_2   = $( '#mailing_address_2' ).val();
2643				data.address.mailing_city        = $( '#mailing_city' ).val();
2644				data.address.mailing_state       = $( '#mailing_state' ).val();
2645				data.address.mailing_country     = $( '#mailing_country' ).val();
2646				data.address.mailing_postal_code = $( '#mailing_postal_code' ).val();
2647				data.order.total                 = data.order.total + data.order.donation;
2648			}
2649
2650			if ( themeCheckout.$dndCheckbox.is( ':checked' ) ) {
2651				data.dnd = {};
2652				if ( themeCheckout.$tipPercent.val() !== '' ) {
2653					data.dnd.tip_percent = themeCheckout.$tipPercent.val();
2654				} else if ( themeCheckout.$phone.val() !== '' ) {
2655					data.dnd.phone = themeCheckout.$phone.val();
2656				}
2657			}
2658
2659			if ( $( 'body' ).hasClass( 'page-template-donate-template' ) ) {
2660				data.order.total = data.order.donation;
2661			}
2662
2663			if ( $( '[name="h-captcha-response"]' ).length > 0 ) {
2664				data.hCaptchaResponse = $( '[name="h-captcha-response"]' ).val();
2665
2666				$( document ).on( 'checkout-error', function ( event, obj ) {
2667					hcaptcha.reset();
2668				} );
2669			}
2670
2671			// Include card payment method data if applicable
2672			if ( themeCheckout.paymentMethod === 'cc' || themeCheckout.paymentMethod === 'gc_partial' ) {
2673				data.paymentMethodData = themeCheckout.cardPaymentMethod;
2674			}
2675
2676			// Include gift card data if applicable
2677			if ( themeCheckout.paymentMethod === 'gc_partial' || themeCheckout.paymentMethod === 'gc' ) {
2678				data = themeCheckout.getGiftCardData( data );
2679			}
2680
2681			if ( typeof $.applyFilter !== 'undefined' ) {
2682				data = $.applyFilter( 'filmbot-modify-checkout-data', data );
2683			}
2684
2685			return data;
2686		},
2687
2688		/**
2689		 * Gathers gift card data from the checkout form.
2690		 *
2691		 * @param data
2692		 * @returns object
2693		 */
2694		getGiftCardData: function ( data ) {
2695			data.gcAccount = themeCheckout.$gcInput.val().replace( /\s/g, '' );
2696			data.gcCvv     = themeCheckout.$gcCvvInput.val();
2697
2698			return data;
2699		},
2700
2701		/**
2702		 * Toggles the availability of the quantity selectors in the checkout form.
2703		 *
2704		 * @param isAvailable
2705		 */
2706		toggleCheckoutFormElementsAvailability: function ( isAvailable = true ) {
2707
2708			// Toggle availability of ticket quantity selectors.
2709			themeCheckout.$quantitySelect.prop( 'disabled', !isAvailable );
2710
2711			// Toggle availability of seat ticket type selector if it exists
2712			themeCheckout.$seatTicketTypeSelect.prop( 'disabled', !isAvailable );
2713
2714			// Toggle availability of donation amount input if it exists
2715			const $donationAmountInput = $( '#donation-amount' );
2716			if ( $donationAmountInput.length ) {
2717				$donationAmountInput.prop( 'disabled', !isAvailable );
2718			}
2719
2720			// Toggle availability of membership signup inputs if they exist
2721			const $membershipInfoInputs = $( '.membership-signup-info input, .membership-signup-info select' );
2722			if ( $membershipInfoInputs.length ) {
2723				$membershipInfoInputs.prop( 'disabled', !isAvailable );
2724			}
2725
2726			// Toggle availability of membership signup inputs if they exist
2727			const $ticketPackInputs = $( '.ticket-pack-purchase-info input, .ticket-pack-purchase-info select' );
2728			if ( $ticketPackInputs.length ) {
2729				$ticketPackInputs.prop( 'disabled', !isAvailable );
2730			}
2731		},
2732
2733		/**
2734		 * Sets the checkout submission status
2735		 * and toggles the availability of the quantity selectors and ticket type selectors accordingly.
2736		 *
2737		 * @param isSubmitted
2738		 */
2739		setIsCheckoutSubmitted: function ( isSubmitted = true ) {
2740
2741			themeCheckout.checkoutSubmitted = isSubmitted;
2742			themeCheckout.toggleCheckoutFormElementsAvailability( !isSubmitted );
2743
2744			const message = "Processing... Don't leave or refresh the page.";
2745
2746			if ( isSubmitted ) {
2747				// Set in-progress message.
2748				themeCheckout.setSubmitError( message );
2749				themeCheckout.$submitError.addClass( 'success-message' );
2750			} else if ( themeCheckout.$submitError.text() === message ) {
2751				// Clear in-progress message if container is not used for other errors.
2752				themeCheckout.setSubmitError( message );
2753				themeCheckout.$submitError.removeClass( 'success-message' );
2754			}
2755		},
2756
2757		/**
2758		 * Submits the checkout form to create purchase.
2759		 *
2760		 * @returns {boolean}
2761		 */
2762
2763		submitCheckout: function () {
2764			// Do not submit checkout if the timer exists and has expired
2765			// but the blocking message has not been shown by any reason.
2766			if ( themeCheckout.showReservationExpiredMessageIfTImerHasExpired() ) {
2767				return false;
2768			}
2769
2770			themeCheckout.setIsCheckoutSubmitted( true );
2771
2772			var data = themeCheckout.getData();
2773
2774			$.ajax( {
2775				url: window.ajaxurl || window.themeSettings.ajaxUrl,
2776				method: 'POST',
2777				timeout: 60000,
2778				data: data,
2779				success: function ( response ) {
2780					if ( response.success && response.orderDetails ) {
2781						// Regular one-time payment that doesn't require payment method authorization.
2782						themeCheckout.setIsCheckoutSubmitted( false );
2783						themeCheckout.orderSuccess( response.orderDetails );
2784						$( document ).trigger( 'checkout-success', { type: 'success', details: response.orderDetails } );
2785					} else {
2786						// some error...
2787						themeCheckout.setIsCheckoutSubmitted( false );
2788
2789						themeCheckout.handleCheckoutErrorResponse( response, response.data );
2790					}
2791				},
2792				error: function ( xhr, status, error ) {
2793					themeCheckout.orderError( xhr, status, error, data );
2794				}
2795			} );
2796			return false;
2797		},
2798
2799		onPaymentAuthorized: function ( initResponse ) {
2800
2801			themeCheckout.completePaymentMethodRequest( true );
2802
2803			const membership = initResponse.orderDetails.membership;
2804			if ( membership && membership.type === 'monthly' ) {
2805				// If the payment is confirmed, finalize the subscription.
2806				themeCheckout.finalizeSubscriptionPurchase( initResponse.orderDetails.postId );
2807			} else {
2808				// If the payment is confirmed, complete the authenticated payment.
2809				themeCheckout.capturePaymentAndFinalizePurchase( initResponse.orderDetails.postId );
2810			}
2811		},
2812
2813		/**
2814		 * Initializes the purchase process for payments that require confirmation (Credit Card, Apple Pay, Google Pay).
2815		 *
2816		 * @returns {boolean}
2817		 */
2818		initializePurchase: function () {
2819			// Do not initialize purchase if the timer exists and has expired
2820			// but the blocking message has not been shown by any reason.
2821			if ( themeCheckout.showReservationExpiredMessageIfTImerHasExpired() ) {
2822				themeCheckout.completePaymentMethodRequest( false );
2823				return false;
2824			}
2825
2826			// Disable the form first time, or again if previous payment attempt has failed.
2827			themeCheckout.disableForm();
2828
2829			themeCheckout.setIsCheckoutSubmitted( true );
2830
2831			var data    = themeCheckout.getData();
2832			data.action = 'filmbot_initialize_purchase';
2833
2834			// Apply the same order ID for the purchase initialization request
2835			// in case previous payment confirmation attempt has failed.
2836			if ( themeCheckout.order.order_id ) {
2837				data.order_id = themeCheckout.order.order_id;
2838			}
2839
2840			$.ajax( {
2841				url: window.ajaxurl || window.themeSettings.ajaxUrl,
2842				method: 'POST',
2843				timeout: 60000,
2844				data: data,
2845				success: function ( response ) {
2846
2847					// Display error if initialization failed,
2848					if ( !response.success || !response.orderDetails || !response.client_secret ) {
2849						themeCheckout.setIsCheckoutSubmitted( false );
2850
2851						themeCheckout.handleCheckoutErrorResponse( response, response.data, true );
2852
2853						themeCheckout.completePaymentMethodRequest( false );
2854						return;
2855					}
2856
2857					// otherwise, proceed with payment confirmation
2858					themeCheckout.order.order_id = response.orderDetails.postId;
2859
2860					// If payment status is requires_capture, proceed to capture
2861					if ( response.payment_status === 'requires_capture' ) {
2862						// Finalize the payment method as payment is already authorized but requires capture.
2863						themeCheckout.onPaymentAuthorized( response );
2864						return;
2865					}
2866
2867					// If payment status is succeeded, display an error and do not proceed
2868					if ( response.payment_status === 'succeeded' ) {
2869						themeCheckout.orderError( {}, null, {}, response );
2870						return;
2871					}
2872
2873					// If this is a one-time payment that requires confirmation, confirm the card payment first and proceed to order finalization.
2874					themeCheckout.confirmCardPayment( response.client_secret, () => {
2875
2876						// Finalize the payment method request as successful.
2877						themeCheckout.onPaymentAuthorized( response );
2878
2879					}, () => {
2880
2881						// Finalize the payment method request as failed.
2882						themeCheckout.completePaymentMethodRequest( false );
2883					} );
2884				},
2885				error: function ( xhr, status, error ) {
2886					// No charge attempted yet — re-enable so the user can retry.
2887					themeCheckout.completePaymentMethodRequest( false );
2888					themeCheckout.setIsCheckoutSubmitted( false );
2889					themeCheckout.enableForm( 'There was a connection error. Your order is incomplete, please check your internet connection and try again.' );
2890					console.error( xhr, status, error );
2891					themeCheckout.triggerSentryWithExtras( '[ Initialize Purchase connection error ] ' + error, { xhr, status } );
2892				}
2893			} );
2894			return false;
2895		},
2896		orderError: function ( xhr, status, error, payload ) {
2897			// timeout or server error on our end... don't re-enable the form, just message it
2898			// as we can't actually say if the charge went through or not.
2899			var errorButtonText  = 'CONNECTION ISSUE';
2900			var errorMessageText = 'Your order may have been successful, please check your email for confirmation. If you don\'t see it within a few minutes, contact us.';
2901			var sendPayload      = false;
2902
2903			// Check for specific errors
2904			if ( xhr.responseJSON && xhr.responseJSON.data ) {
2905				errorMessageText = '';
2906				$.each( xhr.responseJSON.data, function ( i, err ) {
2907					// If the error is a nonce error, update the error message accordingly
2908					if ( err.code === 'rest_cookie_invalid_nonce' ) {
2909						errorButtonText  = 'ORDER FAILED';
2910						errorMessageText = 'Your session has expired, please refresh the page and try again';
2911						sendPayload      = true;
2912					}
2913				} );
2914			}
2915
2916			themeCheckout.setIsCheckoutSubmitted( false );
2917			themeCheckout.$submitError.attr( 'role', 'alert' ).text( errorMessageText );
2918			themeCheckout.$submitButton.prop( 'value', errorButtonText );
2919
2920			$( document ).trigger( 'checkout-error', { type: 'error', details: error } );
2921
2922			console.error( xhr, status, error );
2923
2924			var extras = {
2925				xhr: xhr,
2926				status: status,
2927			};
2928
2929			if ( sendPayload ) {
2930				extras.payload = payload;
2931			}
2932
2933			themeCheckout.triggerSentryWithExtras( '[ Checkout error ] ' + error, extras );
2934		},
2935		orderSuccess: function ( orderDetails ) {
2936			if ( orderDetails.membership && orderDetails.membership.scheduled_for_renewal ) {
2937				var $confirmationMessage = themeCheckout.$orderConfirmation.find( '.membership-confirmation-message' );
2938
2939				if ( $confirmationMessage ) {
2940					$confirmationMessage.remove();
2941				}
2942			}
2943
2944			var $ticketsRow = themeCheckout.$orderConfirmation.find( '.tickets_quantity_subtotal' ).remove();
2945			var $orderData  = themeCheckout.$orderConfirmation.find( '.order-data' );
2946
2947			$( '#order-id' ).text( orderDetails.orderId );
2948			$( '.order-email' ).text( orderDetails.customerEmail ).attr( 'href', `mailto:${orderDetails.customerEmail}` );
2949
2950			$.each( orderDetails.totals.items, function ( i, item ) {
2951				$ticketsRow.clone().find( 'span' ).html( item.html_description ).appendTo( $orderData.find( '.tickets .data' ) );
2952			} );
2953
2954			$orderData.find( '.booking_fee_total' ).text( orderDetails.totals.booking_fees.toFixed( 2 ) );
2955			$orderData.find( '.total_purchase_price' ).text( orderDetails.totals.total.toFixed( 2 ) );
2956
2957			themeCheckout.stopTimer();
2958
2959			$( '.dnd-pickup-instructions' ).hide();
2960
2961			themeCheckout.$checkout.hide();
2962			themeCheckout.$membershipSignup.hide();
2963			themeCheckout.$orderConfirmation.show();
2964			themeCheckout.$ticketPackPurchase.hide();
2965
2966			if ( themeCheckout.$dndCheckbox.is( ':checked' ) ) {
2967				$( '.dnd-pickup-instructions' ).show();
2968			}
2969
2970			$( 'html, body' ).animate( {
2971				scrollTop: 0
2972			}, 500 );
2973		},
2974
2975		/**
2976		 * Retrieves a payment intent with up to 3 retries (1s, 2s, 4s backoff) on connection errors.
2977		 *
2978		 * @param {string} clientSecret
2979		 * @returns {PaymentIntent|null}
2980		 */
2981		retrievePaymentIntentWithRetry: async function ( clientSecret ) {
2982			const retryDelays = [1000, 2000, 4000];
2983			const maxAttempts = 3;
2984
2985			for ( let attempt = 0; attempt < maxAttempts; attempt++ ) {
2986				await new Promise( resolve => setTimeout( resolve, retryDelays[attempt] ) );
2987
2988				const result = await themeCheckout.stripe.retrievePaymentIntent( clientSecret );
2989
2990				if ( result.paymentIntent ) {
2991					return result.paymentIntent;
2992				}
2993
2994				// Only retry on connection errors; any other error won't resolve on retry.
2995				if ( !result.error || result.error.type !== 'api_connection_error' ) {
2996					return null;
2997				}
2998			}
2999
3000			return null;
3001		},
3002
3003		/**
3004		 * Handles an api_connection_error from confirmCardPayment.
3005		 * Checks if confirmation went through by retrieving the intent; proceeds to finalization if so,
3006		 * otherwise re-enables the form for retry.
3007		 *
3008		 * @param {object}   stripeError
3009		 * @param {string}   clientSecret
3010		 * @param {Function} callback
3011		 * @param {Function} errorCallback
3012		 */
3013		onCardConfirmationConnectionError: async function ( stripeError, clientSecret, callback, errorCallback ) {
3014			const paymentIntent = await themeCheckout.retrievePaymentIntentWithRetry( clientSecret );
3015
3016			if ( paymentIntent && paymentIntent.status === 'requires_capture' ) {
3017				callback();
3018				return;
3019			}
3020
3021			// Could not confirm intent status — re-enable so the user can retry.
3022			const message = 'We are experiencing issues connecting to our payments provider. Please check your internet connection.';
3023			themeCheckout.setIsCheckoutSubmitted( false );
3024			themeCheckout.enableForm( message );
3025			$( document ).trigger( 'checkout-error', { type: 'error', details: message } );
3026			themeCheckout.triggerSentryWithExtras( '[ Payment confirmation connection error ] ' + stripeError.message, {
3027				error: stripeError,
3028				orderId: themeCheckout.order.order_id || null,
3029			} );
3030			if ( errorCallback ) {
3031				errorCallback();
3032			}
3033		},
3034
3035		/**
3036		 * Handles a Stripe error from confirmCardPayment (e.g. card declined).
3037		 * Re-enables the form with the error message.
3038		 *
3039		 * @param {object}   stripeError
3040		 * @param {Function} errorCallback
3041		 */
3042		onCardConfirmationError: function ( stripeError, errorCallback ) {
3043			themeCheckout.triggerSentryWithExtras( '[ Payment confirmation error ] ' + stripeError.message, {
3044				error: stripeError,
3045				orderId: themeCheckout.order.order_id || null,
3046			} );
3047			themeCheckout.setIsCheckoutSubmitted( false );
3048			themeCheckout.enableForm( stripeError.message );
3049			$( document ).trigger( 'checkout-error', { type: 'error', details: stripeError.message } );
3050			console.error( '[ Payment Confirmation error ] ' + stripeError.message, stripeError );
3051			if ( errorCallback ) {
3052				errorCallback();
3053			}
3054		},
3055
3056		/**
3057		 * Confirms a card payment using Stripe.js.
3058		 */
3059		confirmCardPayment: async function ( clientSecret, callback, errorCallback ) {
3060
3061			// Use selected payment method from the Stripe Payment Request API event if available,
3062			// otherwise use the card details entered into the Stripe Elements.
3063			const paymentMethod = themeCheckout.stripePaymentRequestEvent?.paymentMethod?.id || {
3064				card: themeCheckout.cardNumber,
3065				billing_details: {
3066					name: $( '#first_name' ).val() + ' ' + $( '#last_name' ).val(),
3067				},
3068			};
3069
3070			themeCheckout.stripe.confirmCardPayment( clientSecret, { payment_method: paymentMethod } )
3071				.then( async resp => {
3072					if ( resp.error ) {
3073						if ( resp.error.type === 'api_connection_error' ) {
3074							await themeCheckout.onCardConfirmationConnectionError( resp.error, clientSecret, callback, errorCallback );
3075						} else {
3076							themeCheckout.onCardConfirmationError( resp.error, errorCallback );
3077						}
3078					} else {
3079						callback();
3080					}
3081				} );
3082		},
3083
3084		/**
3085		 * Finalizes a subscription purchase after successful payment confirmation.
3086		 *
3087		 * @param orderId
3088		 */
3089		finalizeSubscriptionPurchase: async function ( orderId ) {
3090
3091			const data = {
3092				action: 'filmbot_finalize_subscription_purchase',
3093				_ajax_nonce: $( '#filmbot_checkout_nonce' ).val(),
3094				order_id: orderId,
3095			};
3096
3097			$.ajax( {
3098				url: window.ajaxurl || window.themeSettings.ajaxUrl,
3099				method: 'POST',
3100				timeout: 60000,
3101				data: data,
3102				success: function ( response ) {
3103					themeCheckout.setIsCheckoutSubmitted( false );
3104					if ( response.success ) {
3105						themeCheckout.orderSuccess( response.orderDetails );
3106					} else {
3107						// some error...
3108						themeCheckout.handleCheckoutErrorResponse( response, response.data[0] );
3109					}
3110				},
3111				error: function ( xhr, status, error ) {
3112					themeCheckout.orderError( xhr, status, error );
3113				},
3114				finally: function () {
3115					themeCheckout.setIsCheckoutSubmitted( false );
3116				}
3117			} );
3118		},
3119
3120		/**
3121		 * Handles error responses from checkout-related AJAX requests.
3122		 *
3123		 * @param response
3124		 * @param errorMessage
3125		 * @param enableForm
3126		 */
3127		handleCheckoutErrorResponse: function ( response, errorMessage, enableForm = false ) {
3128			errorMessage = errorMessage || 'Unknown Error';
3129
3130			if ( enableForm ) {
3131				themeCheckout.enableForm( errorMessage );
3132			} else {
3133				themeCheckout.setSubmitError( errorMessage );
3134			}
3135
3136			$( document ).trigger( 'checkout-error', { type: 'error', details: errorMessage } );
3137			console.error( '[ Checkout error ] ' + errorMessage, response );
3138
3139			themeCheckout.triggerSentryWithExtras( '[ Checkout error ] ' + errorMessage, {
3140				response: response,
3141			} );
3142		},
3143
3144		/**
3145		 * Captures a payment and finalizes the purchase after successful payment confirmation.
3146		 *
3147		 * @param orderId
3148		 */
3149		capturePaymentAndFinalizePurchase: async function ( orderId ) {
3150			var data = themeCheckout.getData();
3151
3152			data.action   = 'filmbot_finalize_purchase';
3153			data.order_id = orderId;
3154
3155			$.ajax( {
3156				url: window.ajaxurl || window.themeSettings.ajaxUrl,
3157				method: 'POST',
3158				timeout: 60000,
3159				data: data,
3160				success: function ( response ) {
3161					themeCheckout.setIsCheckoutSubmitted( false );
3162					if ( response.success ) {
3163						themeCheckout.orderSuccess( response.orderDetails );
3164					} else {
3165						// some error...
3166						const hasReservations = themeCheckout.$seatsData && !jQuery.isEmptyObject( themeCheckout.$seatsData );
3167						themeCheckout.handleCheckoutErrorResponse( response, response.data[0], hasReservations );
3168					}
3169				},
3170				error: function ( xhr, status, error ) {
3171					themeCheckout.orderError( xhr, status, error );
3172				},
3173				finally: function () {
3174					themeCheckout.setIsCheckoutSubmitted( false );
3175				}
3176			} );
3177		},
3178		scrollUp: function () {
3179			if ( themeCheckout.$isMobile.matches ) {
3180				$( 'html, body' ).animate( {
3181					scrollTop: 0
3182				}, 500 );
3183			}
3184		},
3185		orderHasTickets: function () {
3186			for ( var ticketType in themeCheckout.order.tickets ) {
3187				if ( themeCheckout.order.tickets[ticketType] > 0 )
3188					return true;
3189			}
3190			return false;
3191		},
3192		// Returns true when the order is free (total === 0) for any checkout type —
3193		// tickets, memberships, or ticket packs.
3194		orderIsFreeCheckout: function () {
3195			if ( themeCheckout.order.total !== 0 ) {
3196				return false;
3197			}
3198			return themeCheckout.orderHasTickets()
3199				|| window.themeUx.$body.hasClass( 'membership-payment' )
3200				|| window.themeUx.$body.hasClass( 'membership-renew' )
3201				|| !!themeCheckout.$ticketPackPurchase.length;
3202		},
3203
3204		/*
3205		 * Hides credit card fields and selects cash payment method for free checkouts,
3206		 * and shows them again when not a free checkout.
3207		 *
3208		 * @returns void
3209		 */
3210		maybeToggleCreditCardFields: function () {
3211			if ( themeCheckout.orderIsFreeCheckout() ) {
3212				themeCheckout.hideCreditCardFields();
3213				themeCheckout.$pmMethodSelect.hide();
3214				$( '.payment-section-title' ).hide();
3215
3216				// If a gift card is covering the full cost,
3217				// don't handle this as a free/cash order — keep the gift card as the payment method.
3218				if ( themeCheckout.paymentMethod !== 'gc' && themeCheckout.paymentMethod !== 'gc_partial' ) {
3219					themeCheckout.paymentMethod = 'cash';
3220					themeCheckout.$pmMethodSelect.find( 'select' ).val( 'cash' );
3221				}
3222
3223				return;
3224			}
3225
3226			// Snapshot before show() — an admin may have explicitly chosen cash on a paid order.
3227			var keepCash = themeCheckout.$pmMethodSelect.length
3228				&& themeCheckout.$pmMethodSelect.is( ':visible' )
3229				&& themeCheckout.$pmMethodSelect.find( 'select' ).val() === 'cash';
3230
3231			if ( !keepCash ) {
3232				themeCheckout.$pmMethodSelect.find( 'select' ).val( 'cc' );
3233				themeCheckout.showCreditCardFields();
3234			}
3235
3236			// Keep hidden when a wallet method (Apple/Google Pay) is active.
3237			if ( keepCash || themeCheckout.isManualCreditCardPayment() ) {
3238				themeCheckout.$pmMethodSelect.show();
3239			}
3240
3241			// If a gift card is covering part of the cost, don't override it — keep gc_partial as the payment method.
3242			if ( keepCash || (themeCheckout.paymentMethod !== 'gc' && themeCheckout.paymentMethod !== 'gc_partial') ) {
3243				themeCheckout.paymentMethod = keepCash ? 'cash' : 'cc';
3244			}
3245
3246			$( '.payment-section-title' ).show();
3247		},
3248		orderHasPaidTickets: function () {
3249			var hasPaidTickets = false;
3250			$.each( themeCheckout.$seatTicketTypeSelect, function ( i, ticketTypeSelect ) {
3251				var ticketType = $( 'option:selected', ticketTypeSelect ).data( 'ticket_type' );
3252				if ( ticketType.price > 0 ) {
3253					hasPaidTickets = true;
3254					return;
3255				}
3256			} );
3257			return hasPaidTickets;
3258		},
3259		getShortSeatMapList: function () {
3260			var seatsData = themeCheckout.$hall;
3261			$.each( seatsData.sections, function ( index, section ) {
3262				if ( section ) {
3263					$.each( section.rows, function ( index, rows ) {
3264						if ( rows ) {
3265							$.each( rows.seats, function ( index, seatsList ) {
3266								if ( seatsList['display_id'] ) {
3267									themeCheckout.$shortSeatsList.push( {
3268										display_id: seatsList['display_id'],
3269										seat_address: seatsList['seat_address']
3270									} );
3271								}
3272							} )
3273						}
3274					} )
3275				}
3276			} );
3277			return themeCheckout.$shortSeatsList;
3278		},
3279		changeTextCheckoutButton: function ( items ) {
3280			var reservedSeatsMapped = [],
3281				reservedSeats       = '';
3282
3283			function getNameSeat( seat ) {
3284				return themeCheckout.$shortSeatsList.find( displayId => displayId.seat_address === seat ).display_id;
3285			}
3286
3287			$.each( items, function ( index, item ) {
3288				var newSeat = getNameSeat( item );
3289				reservedSeatsMapped.push( newSeat );
3290			} );
3291			if ( !themeCheckout.ADAMode && items.length ) {
3292				reservedSeats = reservedSeatsMapped.join( ', ' );
3293				themeCheckout.$checkoutSeatsModal.attr( 'disabled', false ).text( "Reserve Seats: " + reservedSeats );
3294			} else {
3295				themeCheckout.$checkoutSeatsModal.attr( 'disabled', 'disable' ).text( "Checkout" );
3296			}
3297		},
3298		recheckShowtimeSelect: function () {
3299			var url    = new URL( window.location.href );
3300			url.search = '';
3301			var urlStr = url.toString();
3302			if ( themeCheckout.$rsShowtimeSelect.val() !== urlStr ) {
3303				themeCheckout.$rsShowtimeSelect.val( urlStr );
3304			}
3305		},
3306		startApplyingDiscountCode( params ) {
3307			themeCheckout.$discountCodeError.html( '' ).css( 'visibility', 'hidden' );
3308			themeCheckout.$discountCodeError.parent().removeClass( 'large-error-message' );
3309			themeCheckout.$discountCodeSuccess.html( '' ).css( 'visibility', 'hidden' );
3310			if ( themeCheckout.$discountCode.prop( 'readonly' ) === true && themeCheckout.$discountCodeApply.html() === 'REMOVE' && !params?.reapplyDiscountCode ) {
3311				themeCheckout.membershipCodeRemovedForEmail = themeCheckout.$emailInput.val();
3312				themeCheckout.removeDiscountCode();
3313				themeCheckout.maybeReapplyGiftCard();
3314				return false;
3315			}
3316			themeCheckout.applyDiscountCode( params );
3317		},
3318		checkEmailMaybeApplyMembershipCode: function () {
3319			// Feature disabled — no lookup nonce in the DOM.
3320			if ( !themeCheckout.$getCodeLookupNonce.length ) {
3321				return;
3322			}
3323
3324			// Another discount request is already in process.
3325			if ( themeCheckout.requestingDiscountCode || themeCheckout.applyingDiscountCode ) {
3326				return;
3327			}
3328
3329			// A manually-applied promo code is active; don't overwrite it.
3330			if ( !themeCheckout.appliedCodeForEmail && themeCheckout.order.discount > 0 ) {
3331				return;
3332			}
3333
3334			const hasError = themeCheckout.checkEmailInput();
3335
3336			// Email changed since last auto-applied membership lookup — clear the stale code.
3337			// Guard on appliedCodeForEmail ensures we never remove a manually-applied promo code,
3338			// since appliedCodeForEmail is only set when a membership code is auto-applied.
3339			if ( themeCheckout.appliedCodeForEmail && themeCheckout.appliedCodeForEmail !== themeCheckout.$emailInput.val() ) {
3340				themeCheckout.removeDiscountCode();
3341			}
3342
3343			// Valid email with no code applied — attempt membership lookup,
3344			// unless the user already explicitly removed the code for this email.
3345			if ( !hasError && !themeCheckout.appliedCodeForEmail && themeCheckout.$emailInput.val() !== themeCheckout.membershipCodeRemovedForEmail ) {
3346				themeCheckout.getMembershipDiscountCode();
3347			}
3348		},
3349		getMembershipDiscountCode: function () {
3350			if ( themeCheckout.requestingDiscountCode ) {
3351				return false;
3352			}
3353
3354			var data = {
3355				action: 'fb_membership_lookup_code',
3356				_ajax_nonce: themeCheckout.$getCodeLookupNonce.val(),
3357				email: themeCheckout.$emailInput.val(),
3358			};
3359
3360			themeCheckout.requestingDiscountCode = true;
3361			themeCheckout.$discountCode.prop( 'readonly', true );
3362
3363			themeCheckout.$discountCodeError.html( '' ).css( 'visibility', 'hidden' );
3364			themeCheckout.$discountCodeError.parent().removeClass( 'large-error-message' );
3365
3366			$.ajax( {
3367				url: window.ajaxurl || window.themeSettings.ajaxUrl,
3368				method: 'POST',
3369				timeout: 60000,
3370				data: data,
3371				success: function ( response ) {
3372					if ( response.success && response.discount_code && themeCheckout.$discountCodeRow.length ) {
3373						// Don't overwrite a manually-applied promo code even if lookup found a match.
3374						if ( !themeCheckout.appliedCodeForEmail && themeCheckout.$discountCode.hasClass( 'discount-code--applied' ) ) {
3375							return;
3376						}
3377						themeCheckout.$discountCodeRow.addClass( 'toggle--opened' );
3378						themeCheckout.$discountCodeField.val( response.discount_code );
3379						themeCheckout.startApplyingDiscountCode( {
3380							successMessage: 'Your membership code was applied',
3381							isMembershipLookup: true,
3382						} );
3383					}
3384				},
3385				error: function ( xhr, status, error ) {
3386					themeCheckout.triggerSentryWithExtras( '[ Membership discount code lookup error ] ' + error, {
3387						xhr: xhr,
3388						status: status,
3389						email: themeCheckout.$emailInput.val()
3390					} );
3391				},
3392				complete: function () {
3393					themeCheckout.requestingDiscountCode = false;
3394					themeCheckout.$discountCode.prop( 'readonly', false );
3395				}
3396			} );
3397		}
3398	};
3399
3400})( window.jQuery, window.Stripe );

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.