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.