1'use strict'; 2 3(function ( $ ) { 4 $.themeCheckout = { 5 6 stripe: null, 7 card: null, 8 elements: null, 9 order_id: null, 10 checkoutSubmitted: false, 11 submitCheckout: () => {}, 12 13 /** 14 * Initializes the checkout process. 15 */ 16 init: function () { 17 $.themeCheckout.initProps(); 18 $.themeCheckout.attachEvents(); 19 $.themeCheckout.initStripeCardElement(); 20 }, 21 22 /** 23 * Initializes properties for the checkout process. 24 */ 25 initProps: function () { 26 $.themeCheckout.$pmSelector = $( '#pmt-method' ); 27 $.themeCheckout.$pmMethodSelect = $( '#payment-method-select' ); 28 $.themeCheckout.$ccPaymentMethodSelector = $( '.payment-method-type-selector' ); 29 $.themeCheckout.$ccPaymentMethodSelectorItem = $( '.payment-method-option' ); 30 $.themeCheckout.$ccField = $( '#payment-form' ); 31 $.themeCheckout.$paymentEmail = $( '#email' ); 32 $.themeCheckout.$terms = $( '#terms' ); 33 $.themeCheckout.$submitButton = $( '#submit-purchase' ); 34 $.themeCheckout.$cardErrors = $( '#card-errors' ); 35 $.themeCheckout.hasAdditionalpmTypeS = false; 36 $.themeCheckout.ccMethod = 'manual-cc'; 37 38 $.themeCheckout.stripePaymentRequestEvent = null; 39 }, 40 41 /** 42 * Triggers a Sentry message with the given message and extras. 43 * 44 * @param message 45 * @param extras 46 */ 47 triggerSentryWithExtras: function ( message, extras ) { 48 // Log a Sentry error 49 if ( typeof Sentry !== 'undefined' ) { 50 Sentry.withScope( function ( scope ) { 51 scope.setLevel( 'error' ); 52 53 if ( extras ) { 54 scope.setExtras( extras ); 55 } 56 Sentry.captureMessage( message ); 57 } ); 58 } 59 }, 60 61 /** 62 * Attaches event listeners for payment method selection. 63 */ 64 attachEvents: function () { 65 $.themeCheckout.$ccPaymentMethodSelectorItem.on( 'click', function ( e ) { 66 e.preventDefault(); 67 68 // Do not allow to change payment method if checkout is already submitted 69 if ( $.themeCheckout.checkoutSubmitted ) { 70 return; 71 } 72 73 $.themeCheckout.selectPaymentMethodType( $( this ) ); 74 } ); 75 }, 76 77 /** 78 * Escapes HTML special characters in the given text. 79 * 80 * @returns {string} 81 */ 82 escHtml: function ( text ) { 83 return $( '<div>' ).text( text ).html(); 84 }, 85 86 /** 87 * Checks if the selected payment method is a manual credit card payment. 88 * 89 * @returns {boolean} 90 */ 91 isManualCreditCardPayment: function () { 92 return $.themeCheckout.ccMethod === 'manual-cc'; 93 }, 94 95 /** 96 * Initializes Stripe Card Element on the given element selector. 97 * Requires STRIPE_PUBLIC_KEY and optionally STRIPE_CONNECTED_ACCOUNT_ID to be defined globally. 98 * Adds listeners for card element changes to display validation errors. 99 */ 100 initStripeCardElement: function () { 101 102 if ( $( '#card-element' ).length === 0 ) { 103 return; 104 } 105 106 const options = STRIPE_CONNECTED_ACCOUNT_ID ? { stripeAccount: STRIPE_CONNECTED_ACCOUNT_ID } : {}; 107 const stripe = Stripe( STRIPE_PUBLIC_KEY, options ); 108 const elements = stripe.elements(); 109 110 // Create an instance of the card Element 111 const card = elements.create( 'card' ); 112 113 // Add an instance of the card Element into the `card-element` <div> 114 card.mount( '#card-element' ); 115 116 // Handle real-time validation errors from the card Element. 117 card.addEventListener( 'change', function ( event ) { 118 const displayError = document.getElementById( 'card-errors' ); 119 displayError.textContent = event.error ? event.error.message : ''; 120 } ); 121 122 $.themeCheckout.stripe = stripe; 123 $.themeCheckout.card = card; 124 $.themeCheckout.elements = elements; 125 126 $.themeCheckout.initPaymentRequest(); 127 }, 128 129 /** 130 * Initializes the Payment Request API for additional payment methods like Apple Pay and Google Pay. 131 */ 132 initPaymentRequest: function () { 133 // Skip extra Payment Methods initialization if there is no element for them. 134 $.themeCheckout.paymentRequest = $.themeCheckout.stripe.paymentRequest( { 135 country: window.themeSettings.country_code || 'US', 136 currency: (window.themeSettings.currency_code || 'usd').toLowerCase(), 137 total: { 138 label: 'Total', 139 amount: 0, 140 }, 141 } ); 142 143 // Check availability of the Payment Request API 144 $.themeCheckout.paymentRequest.canMakePayment().then( function ( enabledMethods ) { 145 $.themeCheckout.displayAvailablePaymentMethods( enabledMethods ); 146 }, function () { 147 $.themeCheckout.$ccPaymentMethodSelector.slideUp(); 148 } ); 149 150 // Handle payment 151 $.themeCheckout.paymentRequest.on( 'paymentmethod', async ( ev ) => { 152 $.themeCheckout.stripePaymentRequestEvent = ev; 153 $.themeCheckout.submitCheckout( ev ); 154 } ); 155 156 // Handle closing of the payment request UI 157 $.themeCheckout.paymentRequest.on( 'cancel', async ( ev ) => { 158
159 // Do not display cancellation message if checkout is already submitted 160 if ( $.themeCheckout.checkoutSubmitted ) { 161 return; 162 } 163 164 $.themeCheckout.displayCheckoutError( { error: { message: 'Payment cancelled.' } } ); 165 } ); 166 }, 167 168 /** 169 * Updates the order total in the Payment Request API. 170 * 171 * @param newTotal 172 */ 173 updateOrderTotal: function ( newTotal ) { 174 if ( !$.themeCheckout.paymentRequest ) { 175 return; 176 } 177 178 if ( typeof newTotal === 'string' ) { 179 try { 180 newTotal = parseFloat( newTotal.replace( /[^0-9.-]+/g, '' ) ); 181 } catch ( e ) { 182 newTotal = 0; 183 } 184 } 185 186 // Round to avoid floating point precision issues with Stripe amounts 187 newTotal = Math.round( Math.max( newTotal, 0 ) * 100 ); 188 189 $.themeCheckout.paymentRequest.update( { 190 total: { 191 label: 'Total', 192 amount: newTotal 193 } 194 } ); 195 }, 196 197 /** 198 * Displays available additional payment methods in the UI. 199 * 200 * @param enabledMethods 201 */ 202 displayAvailablePaymentMethods: function ( enabledMethods ) { 203 if ( !enabledMethods ) { 204 return; 205 } 206 207 // Show Payment Method selector 208 if ( enabledMethods.applePay ) { 209 210 // Show Apple Pay option 211 $.themeCheckout.$ccPaymentMethodSelector.find( '.apple-pay' ).removeClass( 'hidden' ); 212 } 213 if ( enabledMethods.googlePay ) { 214 215 // Show Google Pay option 216 $.themeCheckout.$ccPaymentMethodSelector.find( '.google-pay' ).removeClass( 'hidden' ); 217 } 218 219 $.themeCheckout.hasAdditionalPMs = enabledMethods && (enabledMethods.googlePay || enabledMethods.applePay); 220 if ( $.themeCheckout.hasAdditionalPMs && !$.themeCheckout.orderIsFreeCheckout() ) { 221 $.themeCheckout.$ccPaymentMethodSelector.slideDown(); 222 } 223 }, 224 225 /** 226 * Handles selection of payment method type in the UI. 227 * 228 * @param $ccPaymentMethodItem 229 */ 230 selectPaymentMethodType: function ( $ccPaymentMethodItem ) { 231 232 const shouldChangeCCFields = !!$ccPaymentMethodItem; 233 234 $ccPaymentMethodItem = $ccPaymentMethodItem || $.themeCheckout.$ccPaymentMethodSelectorItem.first(); 235 236 // Check selected payment method type 237 $.themeCheckout.ccMethod = $ccPaymentMethodItem.data( 'pm-type' ); 238 239 // Set current payment method as selected 240 $.themeCheckout.$ccPaymentMethodSelectorItem.removeClass( 'selected' ); 241 $ccPaymentMethodItem.addClass( 'selected' ); 242 243 // Hide or show credit card fields based on selected payment method 244 if ( $ccPaymentMethodItem.hasClass( 'credit-card' ) ) { 245 if ( shouldChangeCCFields ) { 246 $.themeCheckout.$ccField.slideDown(); 247 $.themeCheckout.$pmMethodSelect.slideDown(); 248 } 249 } else { 250 if ( shouldChangeCCFields ) { 251 $.themeCheckout.$ccField.slideUp(); 252 $.themeCheckout.$pmMethodSelect.slideUp(); 253 } 254 } 255 }, 256 257 /** 258 * Returns true when the current order total is zero. 259 */ 260 orderIsFreeCheckout: function () { 261 var total = parseFloat( $( '.total_purchase_price' ).text().replace( /[^0-9.-]+/g, '' ) ) || 0; 262 return total === 0; 263 }, 264 265 /** 266 * Shows credit card fields and payment UI for a non-free order. 267 */ 268 showCreditCardFields: function () { 269 if ( $.themeCheckout.isManualCreditCardPayment() ) { 270 $.themeCheckout.$ccField.slideDown(); 271 } 272 if ( $.themeCheckout.hasAdditionalPMs ) { 273 $.themeCheckout.$ccPaymentMethodSelector.slideDown(); 274 } 275 }, 276 277 /** 278 * Hides credit card fields and payment UI for a free order. 279 */ 280 hideCreditCardFields: function () { 281 $.themeCheckout.$ccField.slideUp(); 282 if ( $.themeCheckout.hasAdditionalPMs ) { 283 $.themeCheckout.$ccPaymentMethodSelector.slideUp(); 284 } 285 $.themeCheckout.$pmMethodSelect.hide(); 286 $.themeCheckout.selectPaymentMethodType(); 287 }, 288 289 /** 290 * Shows or hides payment fields based on whether the order is free. 291 */ 292 maybeToggleCreditCardFields: function () { 293 if ( $.themeCheckout.orderIsFreeCheckout() ) { 294 $.themeCheckout.hideCreditCardFields(); 295 $.themeCheckout.$pmSelector.val( 'cash' ); 296 return; 297 } 298 299 // Snapshot before show() â an admin may have explicitly chosen cash on a paid order. 300 var keepCash = $.themeCheckout.$pmMethodSelect.is( ':visible' ) 301 && $.themeCheckout.$pmSelector.val() === 'cash'; 302 303 if ( !keepCash ) { 304 $.themeCheckout.$pmSelector.val( 'cc' ); 305 $.themeCheckout.showCreditCardFields(); 306 } 307 308 // Keep hidden when a wallet method (Apple/Google Pay) is active. 309 if ( keepCash || $.themeCheckout.isManualCreditCardPayment() ) { 310 $.themeCheckout.$pmMethodSelect.show(); 311 } 312 313 $( '.payment-section-title' ).show(); 314 }, 315 316 /** 317 * Completes the Payment Method request based on the success parameter. 318 * 319 * @param success 320 */ 321 completePaymentMethodRequest: function ( success ) { 322 if ( !$.themeCheckout.stripePaymentRequestEvent ) { 323 return; 324 } 325 326 if ( success ) { 327 $.themeCheckout.stripePaymentRequestEvent.complete( 'success' ); 328 } else { 329 $.themeCheckout.stripePaymentRequestEvent.complete( 'fail' ); 330 } 331 $.themeCheckout.stripePaymentRequestEvent = null; 332 }, 333 334 /** 335 * Validates the required fields in the checkout form. 336 * 337 * @param $requiredFields 338 */ 339 validateRequiredFields: function ( $requiredFields ) { 340 if ( typeof $requiredFields.each === 'function' ) { 341 $requiredFields.each( function () { 342 $.themeCheckout.validateCheckoutInputs( $( this ) ); 343 } ); 344 } else { 345 $.each( $requiredFields, function ( i, $field ) { 346 $.themeCheckout.validateCheckoutInputs( $field ); 347 } ); 348 } 349 350 $.themeCheckout.validateCheckoutEmail( $.themeCheckout.$paymentEmail ); 351 352 if ( $.themeCheckout.$terms.length ) { 353 $.themeCheckout.validateCheckoutTermsCheckbox( $.themeCheckout.$terms ); 354 } 355 }, 356 357 /** 358 * Displays checkout error messages to the user. 359 * 360 * @param result 361 */ 362 displayCheckoutError: function ( result ) { 363 // Inform the user if there was an error 364 $.themeCheckout.$cardErrors.html( result.error.message || 'Unknown error' );
365 $.themeCheckout.$submitButton.prop( 'disabled', false ).val( 'Purchase' ); 366 }, 367 368 /** 369 * Submits the purchase request if the form is valid. 370 * 371 * @param $order 372 * @param submitOrderCallback 373 */ 374 submitPurchaseRequestIfValidForm: function ( $order, submitOrderCallback ) { 375 submitOrderCallback = submitOrderCallback || function ( order, result ) {}; 376 var $error = $( '.error' ); 377 $error.next().hide(); 378 379 // Check for validation errors, display them and stop submission 380 if ( $error.length ) { 381 $error.next().show(); 382 $( '.error-message.submit-error' ).text( 'Please fix the errors before continuing.' ).show(); 383 $.themeCheckout.$submitButton.prop( 'disabled', false ).val( 'Purchase' ); 384 return; 385 } 386 387 $( '.error-message.submit-error' ).hide(); 388 389 const isCreditCardPayment = $order.total > 0 && ($.themeCheckout.$pmSelector.length === 0 || $.themeCheckout.$pmSelector.val() === 'cc'); 390 391 // Submit order without payment method if payment method is not credit card 392 if ( !isCreditCardPayment ) { 393 submitOrderCallback( $order, {} ); 394 return; 395 } 396 397 // Submit order with payment method if payment method is credit card 398 if ( $.themeCheckout.isManualCreditCardPayment() ) { 399 $.themeCheckout.stripe.createPaymentMethod( { 400 type: 'card', 401 card: $.themeCheckout.card, 402 billing_details: { 403 name: $( '#first_name' ).val() + ' ' + $( '#last_name' ).val(), 404 email: $( '#email' ).val(), 405 address: { 406 postal_code: $( '#mailing_postal_code' ).val() 407 } 408 } 409 } ).then( function ( result ) { 410 // Submit the checkout and pass the PaymentMethod. 411 submitOrderCallback( $order, result ); 412 } ); 413 } else { 414 415 // Request payment method and then submit checkout for alternative payment methods (Apple Pay, Google Pay) 416 $.themeCheckout.submitCheckout = function ( result ) { 417 submitOrderCallback( $order, result || {} ); 418 }; 419 $.themeCheckout.requestPaymentMethod(); 420 } 421 }, 422 423 /** 424 * Initiates the Payment Method request. 425 */ 426 requestPaymentMethod: function () { 427 if ( $.themeCheckout.paymentRequest ) { 428 $.themeCheckout.paymentRequest.show(); 429 } 430 }, 431 432 /** 433 * Retrieves a payment intent with up to 3 retries (1s, 2s, 4s backoff) on connection errors. 434 * 435 * @param clientSecret 436 * @returns {PaymentIntent|null} 437 */ 438 retrievePaymentIntentWithRetry: async function ( clientSecret ) { 439 const retryDelays = [1000, 2000, 4000]; 440 const maxAttempts = 3; 441 442 for ( let attempt = 0; attempt < maxAttempts; attempt++ ) { 443 await new Promise( resolve => setTimeout( resolve, retryDelays[attempt] ) ); 444 445 const result = await $.themeCheckout.stripe.retrievePaymentIntent( clientSecret ); 446 447 if ( result.paymentIntent ) { 448 return result.paymentIntent; 449 } 450 451 // Only retry on connection errors; any other error won't resolve on retry. 452 if ( !result.error || result.error.type !== 'api_connection_error' ) { 453 return null; 454 } 455 } 456 457 return null; 458 }, 459 460 /** 461 * Handles an api_connection_error from confirmCheckoutCardPayment. 462 * Checks if confirmation went through by retrieving the intent; proceeds to finalization if so, 463 * otherwise re-enables the form for retry. 464 * 465 * @param stripeError 466 * @param clientSecret 467 * @param callback 468 * @param errorCallback 469 */ 470 onCardConfirmationConnectionError: async function ( stripeError, clientSecret, callback, errorCallback ) { 471 const paymentIntent = await $.themeCheckout.retrievePaymentIntentWithRetry( clientSecret ); 472 473 if ( paymentIntent && paymentIntent.status === 'requires_capture' ) { 474 callback(); 475 return; 476 } 477 478 // Could not confirm intent status â re-enable so customer can retry. 479 const message = 'We are experiencing issues connecting to our payments provider. Please check your internet connection.'; 480 $.themeCheckout.$cardErrors.html( message ); 481 $.themeCheckout.setIsCheckoutSubmitted( false ); 482 $( document ).trigger( 'checkout-error', { type: 'error', details: message } ); 483 $.themeCheckout.triggerSentryWithExtras( '[ Payment confirmation connection error ] ' + stripeError.message, { 484 error: stripeError, 485 orderId: $.themeCheckout.order_id || null, 486 } ); 487 if ( errorCallback ) { 488 errorCallback( message ); 489 } 490 }, 491 492 /** 493 * Handles a Stripe error from confirmCheckoutCardPayment (e.g. card declined). 494 * Re-enables the form with the error message. 495 * 496 * @param stripeError 497 * @param errorCallback 498 */ 499 onCardConfirmationError: function ( stripeError, errorCallback ) {
500 $.themeCheckout.triggerSentryWithExtras( '[ Payment confirmation error ] ' + stripeError.message, { 501 error: stripeError, 502 orderId: $.themeCheckout.order_id || null, 503 } ); 504 $( document ).trigger( 'checkout-error', { type: 'error', details: stripeError.message } ); 505 console.error( '[ Payment Confirmation error ] ' + stripeError.message, stripeError ); 506 if ( errorCallback ) { 507 errorCallback( stripeError.message ); 508 } 509 }, 510 511 /** 512 * Confirms a Stripe card payment using the provided client secret. 513 * Calls the provided callback on success or errorCallback on failure. 514 * 515 * @param clientSecret 516 * @param callback 517 * @param errorCallback 518 */ 519 confirmCheckoutCardPayment: async function ( clientSecret, callback, errorCallback ) { 520 const paymentMethod = $.themeCheckout.stripePaymentRequestEvent?.paymentMethod?.id || { 521 card: $.themeCheckout.card, 522 billing_details: { 523 name: $( '#first_name' ).val() + ' ' + $( '#last_name' ).val(), 524 }, 525 }; 526 $.themeCheckout.stripe.confirmCardPayment( clientSecret, { payment_method: paymentMethod } ) 527 .then( async resp => { 528 if ( resp.error ) { 529 if ( resp.error.type === 'api_connection_error' ) { 530 await $.themeCheckout.onCardConfirmationConnectionError( resp.error, clientSecret, callback, errorCallback ); 531 } else { 532 $.themeCheckout.onCardConfirmationError( resp.error, errorCallback ); 533 } 534 } else { 535 callback(); 536 } 537 } ); 538 }, 539 540 /** 541 * Finalizes an authenticated payment for the given order ID. 542 * Sends an AJAX request to the server to complete the payment process. 543 * Calls the provided callback on success or errorCallback on failure. 544 * 545 * @param orderId 546 * @param callback 547 * @param errorCallback 548 */ 549 capturePaymentAndFinalizePurchase: async function ( data, callback, errorCallback ) { 550 551 data.action = data.actions?.finalize || 'filmbot_finalize_purchase'; 552 553 $.ajax( { 554 url: themeSettings.ajaxUrl, 555 method: 'POST', 556 timeout: 60000, 557 data: data, 558 success: function ( response ) { 559 if ( response.success ) { 560 if ( typeof callback === 'function' ) { 561 callback( response.orderDetails ); 562 } 563 } else { 564 // some error... 565 var error = response.data[0] || 'Unknown Error'; 566 567 $.themeCheckout.triggerSentryWithExtras( '[ Finalize purchase error ] ' + error, { 568 error: error, 569 orderId: data.order_id || null, 570 response: response 571 } ); 572 573 if ( typeof errorCallback === 'function' ) { 574 errorCallback( error ); 575 } 576 } 577 }, 578 error: function ( xhr, status, error ) { 579 $.themeCheckout.triggerSentryWithExtras( '[ Finalize purchase AJAX error ]', { 580 xhr: xhr, 581 status: status, 582 error: error, 583 orderId: data.order_id || null 584 } ); 585 586 if ( typeof errorCallback === 'function' ) { 587 errorCallback( "Your order may have been successful, please check your email for confirmation. If you don't see it within a few minutes, contact us." ); 588 $( '#submit-purchase' ).prop( 'disabled', true ).val( 'CONNECTION ISSUE' ); 589 } 590 } 591 } ); 592 }, 593 594 595 /** 596 * Checks the payment method for the given order data 597 * and updates it to 'cash' if the total amount is zero. 598 * 599 * @param data 600 * 601 * @returns object 602 */ 603 checkPaymentMethod: function ( data ) { 604 605 let total = data.order.total; 606 607 total = typeof total === 'string' ? parseFloat( total ) : total; 608 609 if ( total === 0 ) { 610 data.pm = 'cash'; 611 } 612 613 return data; 614 }, 615 616 /** 617 * Submits the checkout data to the server via AJAX. 618 * Handles payment confirmation if required. 619 * Calls the provided callback on success or errorCallback on failure. 620 * 621 * @param data 622 * @param callback 623 * @param errorCallback 624 */ 625 checkoutSubmitRequest: function ( data, callback, errorCallback ) { 626
627 // Do not allow multiple submissions of the checkout form 628 if ( $.themeCheckout.checkoutSubmitted ) { 629 return; 630 } 631 632 data = $.themeCheckout.checkPaymentMethod( data ); 633 634 if ( data.pm === 'cc' ) { 635 $.themeCheckout.initializePurchaseRequest( data, callback, errorCallback ); 636 return; 637 } 638 639 // Disable the submit button to prevent multiple submissions while processing the order. 640 $.themeCheckout.setIsCheckoutSubmitted( true ); 641 642 $.ajax( { 643 url: themeSettings.ajaxUrl, 644 method: 'POST', 645 timeout: 60000, 646 data: data, 647 success: function ( response ) { 648 649 if ( response.success && response.orderDetails ) { 650 if ( typeof callback === 'function' ) { 651 callback( response.orderDetails ); 652 } 653 } else { 654 // some error... 655 $.themeCheckout.triggerSentryWithExtras( '[ Create Purchase error ]', { 656 response: response, 657 orderId: $.themeCheckout.order_id || null 658 } ); 659 660 if ( typeof errorCallback === 'function' ) { 661 errorCallback( response ); 662 } 663 } 664 }, 665 error: $.themeCheckout.orderInitializationError 666 } ); 667 }, 668 669 onPaymentAuthorized: function ( initResponse, data, callback, errorCallback ) { 670 671 $.themeCheckout.completePaymentMethodRequest( true ); 672 673 $.themeCheckout.capturePaymentAndFinalizePurchase( { ...data, order_id: initResponse.order_id }, () => { 674 if ( typeof callback === 'function' ) { 675 callback( initResponse.orderDetails ); 676 } 677 }, ( error ) => { 678 679 $.themeCheckout.completePaymentMethodRequest( false ); 680 681 if ( typeof errorCallback === 'function' ) { 682 errorCallback( { data: error } ); 683 } 684 } ); 685 }, 686 687 /** 688 * Submits the checkout data to the server via AJAX. 689 * Handles payment confirmation if required. 690 * Calls the provided callback on success or errorCallback on failure. 691 * 692 * @param data 693 * @param callback 694 * @param errorCallback 695 */ 696 initializePurchaseRequest: function ( data, callback, errorCallback ) { 697 data.action = data.actions?.initialize || 'filmbot_initialize_purchase'; 698 699 if ( $.themeCheckout.order_id ) { 700 data.order_id = $.themeCheckout.order_id; 701 } 702 703 // Disable the submit button to prevent multiple submissions while processing the order. 704 $.themeCheckout.setIsCheckoutSubmitted( true ); 705 706 $.ajax( { 707 url: themeSettings.ajaxUrl, 708 method: 'POST', 709 timeout: 60000, 710 data: data, 711 success: function ( response ) { 712 if ( response.success && response.orderDetails ) { 713 714 $.themeCheckout.order_id = response.order_id; 715 716 // Display error if initialization failed, 717 if ( response.payment_status === 'requires_capture' ) { 718 // Finalize the payment method as payment is already authorized but requires capture. 719 $.themeCheckout.onPaymentAuthorized( response, data, callback, errorCallback ); 720 return; 721 } 722 723 // If this is a one-time payment that requires confirmation, confirm the card payment first and proceed to order finalization. 724 $.themeCheckout.confirmCheckoutCardPayment( response.client_secret, () => { 725 726 // If the payment is confirmed, complete the authenticated payment. 727 $.themeCheckout.onPaymentAuthorized( response, data, callback, errorCallback ); 728 729 }, ( error ) => { 730 731 $.themeCheckout.completePaymentMethodRequest( false ); 732 733 if ( typeof errorCallback === 'function' ) { 734 errorCallback( { data: error } ); 735 } 736 } ); 737 } else { 738 739 $.themeCheckout.completePaymentMethodRequest( false ); 740 741 // some error... 742 $.themeCheckout.triggerSentryWithExtras( '[ Initialize purchase error ]', { 743 response: response, 744 orderId: $.themeCheckout.order_id || null 745 } ); 746 747 if ( typeof errorCallback === 'function' ) { 748 errorCallback( response ); 749 } 750 } 751 }, 752 error: function ( xhr, status, error ) { 753 // No charge attempted yet â re-enable so customer can retry. 754 $.themeCheckout.completePaymentMethodRequest( false ); 755 $.themeCheckout.setIsCheckoutSubmitted( false ); 756 $( document ).trigger( 'checkout-error', { type: 'error', details: error } ); 757 $( '.error-message.submit-error' ).show().html( 'There was a connection error. Your order is incomplete, please check your internet connection and try again.' ); 758 $.themeCheckout.triggerSentryWithExtras( '[ Initialize Purchase connection error ] ' + error, { xhr, status } ); 759 } 760 } ); 761 }, 762 763 /** 764 * Handles a network error during non-card order submission. 765 * Keeps the form disabled since the order may have completed on the server. 766 * 767 * @param xhr 768 * @param status 769 * @param error 770 */ 771 orderInitializationError: function ( xhr, status, error ) { 772 // timeout or server error on our end... don't re-enable the form, just message it 773 // as we can't actually say if the charge went through or not. 774 $( '.error-message.submit-error' ).text( 'There was an error completing your transaction, please contact us for support.' ); 775 $( '#submit-purchase' ).prop( 'value', 'ORDER FAILED' ); 776 $( document ).trigger( 'checkout-error', { type: 'error', details: error } ); 777 $.themeCheckout.triggerSentryWithExtras( '[ Checkout error ] ' + error, { xhr, status } ); 778 $.themeCheckout.completePaymentMethodRequest( false ); 779 }, 780 781 /** 782 * Displays a confirmation dialog for the given donation amount. 783 * Calls the provided callback if user confirms. 784 * 785 * @param donationAmount 786 * @param callback 787 */ 788 confirmDonationAmount: function ( donationAmount, callback ) { 789 bootbox.confirm( { 790 title: "Confirm donation amount", 791 message: "You've chosen to donate $" + donationAmount + ". Ok to continue?", 792 buttons: { 793 confirm: { label: 'Yes' }, 794 cancel: { label: 'No' } 795 }, 796 callback: function ( confirmed ) { 797 if ( confirmed === true && typeof callback === 'function' ) { 798 callback(); 799 } 800 } 801 } ); 802 }, 803 804 /** 805 * Validates the given email address format. 806 * 807 * @param emailAddress 808 */ 809 isValidCheckoutEmailAddress: function ( emailAddress ) { 810 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,}))$/ ); 811 return pattern.test( emailAddress ); 812 }, 813 814 /** 815 * Validates the checkout email input field. 816 * 817 * @param $paymentEmail 818 */ 819 validateCheckoutEmail: function ( $paymentEmail ) { 820 if ( !$.themeCheckout.isValidCheckoutEmailAddress( $paymentEmail.val() ) ) { 821 $paymentEmail.addClass( 'error' ); 822 } else { 823 $paymentEmail.removeClass( 'error' ); 824 $paymentEmail.next().hide(); 825 } 826 }, 827 828 /** 829 * Validates the given checkout input field for non-empty value. 830 * 831 * @param item 832 */ 833 validateCheckoutInputs: function ( item ) {
834 // Skip disabled or hidden inputs â they are not reachable by the customer. 835 if ( item.prop( 'disabled' ) === true || !item.is( ':visible' ) ) { 836 return; 837 } 838 839 var val = item.val(); 840 item.toggleClass( 'error', $.trim( val ) === '' || val === null ); 841 if ( item.hasClass( 'error' ) ) { 842 item.next().show(); 843 } else { 844 item.next().hide(); 845 } 846 }, 847 848 /** 849 * Validates the terms and conditions checkbox. 850 * 851 * @param $terms 852 */ 853 validateCheckoutTermsCheckbox: function ( $terms ) { 854 var checked = $( 'input[name=terms]' ).prop( 'checked' ); 855 $terms.toggleClass( 'error', checked !== true ); 856 }, 857 858 /** 859 * Displays checkout error messages to the user. 860 * Accepts either a string or an array of error messages. 861 * 862 * @param error 863 */ 864 displayCheckoutErrorsList: function ( error ) { 865 if ( 'string' === typeof error ) { 866 $( '.error-message.submit-error' ).html( error ).show(); 867 } else if ( error.length === 1 ) { 868 $( '.error-message.submit-error' ).html( error[0] ).show(); 869 } else { 870 var messages = `<ul>`; 871 872 $.each( error, function ( i, msg ) { 873 messages += `<li>${msg}</li>`; 874 } ); 875 876 messages += `</ul>`; 877 878 $( '.error-message.submit-error' ).html( messages ).show(); 879 } 880 }, 881 882 /** 883 * Triggers a Sentry error event with the given failed response details. 884 * 885 * @param failedResponse 886 * @param error 887 */ 888 triggerSentryError: function ( failedResponse, error ) { 889 if ( typeof Sentry !== 'undefined' ) { 890 Sentry.withScope( function ( scope ) { 891 scope.setLevel( 'error' ); 892 893 scope.setExtras( { 894 response: failedResponse 895 } ); 896 897 Sentry.captureMessage( '[ Checkout error ] ' + error ); 898 } ); 899 } 900 }, 901 902 /** 903 * Handles order failure by displaying error messages and logging the error details. 904 * 905 * @param failedResponse 906 */ 907 orderFailed: function ( failedResponse ) { 908 // some error... 909 var error = failedResponse.data || 'Unknown Error'; 910 $( document ).trigger( 'checkout-error', { type: 'error', details: error } ); 911 912 console.error( '[ Checkout error ] ' + error, failedResponse ); 913 914 $.themeCheckout.triggerSentryError( failedResponse, error ); 915 916 $.themeCheckout.displayCheckoutErrorsList( error ); 917 918 $.themeCheckout.setIsCheckoutSubmitted( false ); 919 }, 920 921 /** 922 * Toggles the availability of checkout form elements based on the availability parameter. 923 * 924 * @param isAvailable 925 */ 926 toggleCheckoutFormElementsAvailability: function ( isAvailable = true ) { 927 928 // Toggle availability of quantity selectors 929 const $quantitySelectors = $( '.ticket-quantity select' ); 930 if ( $quantitySelectors.length > 0 ) { 931 $quantitySelectors.prop( 'disabled', !isAvailable ); 932 } 933 934 // Toggle availability of payment method select 935 const $pmSelect = $( '#payment-method-select select' ); 936 if ( $pmSelect.length > 0 ) { 937 $pmSelect.prop( 'disabled', !isAvailable ); 938 } 939 940 // Toggle availability of discount code input 941 const $discountCodeInput = $( '#discount-code-input' ); 942 if ( $discountCodeInput.length > 0 ) { 943 $discountCodeInput.prop( 'disabled', !isAvailable ); 944 } 945 946 // Toggle availability of discount code button 947 const $discountCodeButton = $( '.discount-code button' ); 948 if ( $discountCodeButton.length > 0 ) { 949 $discountCodeButton.prop( 'disabled', !isAvailable ); 950 } 951 952 // Toggle availability of "Comp?" checkbox 953 const $isCompCheckbox = $( '#is-comp' ); 954 if ( $isCompCheckbox.length > 0 ) { 955 $isCompCheckbox.prop( 'disabled', !isAvailable ); 956 } 957 958 // Toggle availability of "Donate" checkbox 959 const $donateCheckbox = $( '#donate-me' ); 960 if ( $donateCheckbox.length > 0 ) { 961 $donateCheckbox.prop( 'disabled', !isAvailable ); 962 } 963 964 // Toggle availability of terms checkbox 965 const $termsCheckbox = $( '#terms' ); 966 if ( $termsCheckbox.length > 0 ) { 967 $termsCheckbox.prop( 'disabled', !isAvailable ); 968 } 969 970 // Toggle availability of customer information inputs 971 const $customerInputs = $( '.payment-address input, .payment-address select' ); 972 if ( $customerInputs.length > 0 ) { 973 if ( !isAvailable ) { 974 $customerInputs.prop( 'disabled', true ); 975 } else { 976 $customerInputs.filter( ':not(:hidden)' ).prop( 'disabled', false ); 977 } 978 } 979 980 // Toggle availability of customer information inputs 981 const $membershipInfoInputs = $( '.cart-membership-details input, .cart-membership-details select' ); 982 if ( $membershipInfoInputs.length > 0 ) { 983 $membershipInfoInputs.prop( 'disabled', !isAvailable ); 984 } 985 986 // Toggle availability of customer information inputs 987 const $customDonationAmountInputs = $( '.donation-variants-container input' ); 988 if ( $customDonationAmountInputs.length > 0 ) { 989 $customDonationAmountInputs.prop( 'disabled', !isAvailable ); 990 } 991 }, 992 993 /** 994 * Toggles the availability of the submit button in the checkout form 995 * and resets its text if it is being enabled. 996 * 997 * @param isAvailable 998 */ 999 toggleSubmitButtonAvailability: function ( isAvailable = true ) { 1000 if ( isAvailable ) { 1001 $( '#submit-purchase' ).prop( 'disabled', false ).val( 'Purchase' ); 1002 } else { 1003 $( '#submit-purchase' ).prop( 'disabled', true ).val( 'Please wait...' ); 1004 $( '.error-message.submit-error' ).hide(); 1005 } 1006 }, 1007 1008 /** 1009 * Toggles the availability of Stripe Card Elements 1010 * 1011 * @param isAvailable 1012 */ 1013 toggleCardElementsAvailability: function ( isAvailable = true ) { 1014 if ( $.themeCheckout.card ) { 1015 $.themeCheckout.card.update( { disabled: !isAvailable } ); 1016 } 1017 }, 1018 1019 /** 1020 * Sets the checkout submission status and toggles the availability 1021 * of the form fields accordingly to prevent changes while the checkout is being processed. 1022 * 1023 * @param isSubmitted 1024 */ 1025 setIsCheckoutSubmitted: function ( isSubmitted = true ) { 1026 $.themeCheckout.checkoutSubmitted = isSubmitted; 1027 1028 $.themeCheckout.toggleSubmitButtonAvailability( !isSubmitted ); 1029 $.themeCheckout.toggleCheckoutFormElementsAvailability( !isSubmitted ); 1030 $.themeCheckout.toggleCardElementsAvailability( !isSubmitted ); 1031 }, 1032 }; 1033})( jQuery );
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.