1//fallback if translated strings are not in memory 2if (typeof textError == 'undefined') { 3 var textError = 'Error '; 4 var textTryAgain = ' Please try again.'; 5 var processingPayment = 'Processing your payment...'; 6 var paymentDeclined1 = 'Payment Declined'; 7 var paymentDeclined2 = 'You must approve the payment to complete the order. Click the button to authenticate the payment.'; 8} 9 10// used currently only for stripe iDEAL payments. Will remain blank, unless the 11// pop up window is generated for the iDEAL payment confirmation 12var newWindow; 13var updateControl = 0; 14var paymentPlanType; 15 16// True while a caller is managing the PaymentIntent itself, so the 17// recalc-driven refresh in orders.js stays out of the way. The timer 18// self-clears the flag in case the consumer never sees the recalc. 19let suppressPaymentIntentRefresh = false; 20let suppressPaymentIntentRefreshTimer = null; 21const SUPPRESS_PAYMENT_INTENT_REFRESH_TIMEOUT_MS = 5000; 22 23function armSuppressPaymentIntentRefresh() { 24 suppressPaymentIntentRefresh = true; 25 if (suppressPaymentIntentRefreshTimer) { 26 clearTimeout(suppressPaymentIntentRefreshTimer); 27 } 28 suppressPaymentIntentRefreshTimer = setTimeout(() => { 29 suppressPaymentIntentRefresh = false; 30 suppressPaymentIntentRefreshTimer = null; 31 }, SUPPRESS_PAYMENT_INTENT_REFRESH_TIMEOUT_MS); 32} 33 34function clearSuppressPaymentIntentRefresh() { 35 suppressPaymentIntentRefresh = false; 36 if (suppressPaymentIntentRefreshTimer) { 37 clearTimeout(suppressPaymentIntentRefreshTimer); 38 suppressPaymentIntentRefreshTimer = null; 39 } 40} 41 42 43/** 44 * @returns the `clientSecret` from the new payment intent that was created 45 */ 46function stripeCreatePaymentIntent() { 47 // if already has a payment intent, see if it can be canceled. 48 if($("#orderForm #OrderPaymentIntentSecret").val()){ 49 // Remove old fields synchronously before any async calls, so the 50 // cancel callback can't accidentally remove the new fields created below. 51 let oldId = $("#orderForm #OrderPaymentIntentId").val(); 52 let oldSecret = $("#orderForm #OrderPaymentIntentSecret").val(); 53 $("#orderForm #OrderPaymentIntentId").remove(); 54 $("#orderForm #OrderPaymentIntentSecret").remove(); 55 cancelPaymentIntent(oldId, oldSecret); 56 } 57 return new Promise((resolve) => { 58 var clientSecret = []; 59 $.ajax({ 60 type: "POST", 61 url: buildURL("/orders/ajax_create_payment_intent/"), 62 data: $("#orderForm").serialize(), 63 dataType: "json", 64 success: function (data) { 65 paymentPlanType = data.paymentPlanType; 66 switch (data.status) { 67 case "success": 68 //create fields for payment intent 69 var $form = $("#orderForm"); 70 $form.append($('<input type="hidden" name="data[Order][payment_intent_id]" id="OrderPaymentIntentId">').val(data.id)); 71 $form.append($('<input type="hidden" name="data[Order][payment_intent_secret]" id="OrderPaymentIntentSecret">').val(data.client_secret)); 72 clientSecret = data.client_secret; 73 break; 74 case "fail": 75 case "error": 76 break; 77 } 78 }, 79 complete: function () { 80 resolve(clientSecret); 81 }, 82 }); 83 }); 84} 85 86// will check if the payment intent that was created can be canceled. Can only 87// cancel with specific status defined in the stripe controller 88// When called with oldId/oldSecret, the hidden fields have already been removed 89// by the caller to prevent a race condition with the new PI fields. 90function cancelPaymentIntent(oldId, oldSecret){ 91 let id = oldId || $("#orderForm #OrderPaymentIntentId").val(); 92 let secret = oldSecret || $("#orderForm #OrderPaymentIntentSecret").val(); 93 if(secret && secret.length){ 94 let formData = $("#orderForm").serialize(); 95 if (id) { 96 formData += "&data[Order][payment_intent_id]=" + encodeURIComponent(id); 97 } 98 formData += "&data[Order][payment_intent_secret]=" + encodeURIComponent(secret); 99 $.ajax({ 100 type: "POST", 101 url: buildURL("/orders/ajax_cancel_payment_intent"), 102 data: formData, 103 dataType: "json", 104 error: function(xhr, status, err) { 105 // Cancel is best-effort cleanup; log for debugging but don't interrupt buyer flow. 106 console.error('cancelPaymentIntent failed:', status, err); 107 }, 108 }); 109 } 110} 111 112/** 113 * Stripe Elements Implementation below 114 */ 115var stripe = Stripe(stripePublicKey); 116let elements; 117var clientSecret; 118 119/** 120 * Fetches a payment intent and captures the client secret 121 */ 122async function initialize() {
123 clientSecret = await stripeCreatePaymentIntent(); 124 125 var layout = { 126 type: 'accordion', 127 defaultCollapsed: false, 128 radios: true, 129 spacedAccordionItems: false 130 }; 131 var theme = 'stripe'; 132 var vars = { 133 fontFamily: 'Inter, Open Sans, sans-serif', 134 colorTextPlaceholder: '#ccc' 135 }; 136 137 // check if in admin - remove wallet options for admin 138 if($('#OrderPurchaseLocation').val() === 'back-end'){ 139 appleWalletPayments = 'never'; 140 googleWalletPayments = 'never'; 141 } else { 142 // get the text color for the page and use that on the payment element 143 // also make the background transparent so that it matches the page 144 var eventThemeColor = $('body').data('theme-color'); 145 if (eventThemeColor == 'dark') { 146 theme = 'night'; 147 vars.colorPrimary = '#767994'; 148 } 149 var eventTheme = $('body').attr('class'); 150 if (eventTheme == 'modern-v2') { 151 layout = { 152 type: 'tabs' 153 }; 154 } 155 } 156 157 const appearance = { 158 theme: theme, 159 labels: 'above', 160 variables: vars, 161 }; 162 163 const loader = 'always'; 164 165 elements = stripe.elements({ clientSecret, appearance, loader }); 166 const paymentElement = elements.create("payment", { 167 wallets: { 168 applePay: appleWalletPayments, 169 googlePay: googleWalletPayments, 170 }, 171 layout: layout 172 }); 173 174 // Mount paymentElement if element exists on the page (not waitlist) 175 var paymentElementExists = document.getElementById("payment-element"); 176 if (paymentElementExists) { 177 paymentElement.mount("#payment-element"); 178 } 179 180 paymentElement.on('change', function(event) { 181 if (event.complete) { 182 // element data seems to be correct complete - enable payment button 183 enableSubmit(origSubmitButtonText); 184 } else { 185 // element data does not seem to be complete - disables the payment button 186 disableSubmit(origSubmitButtonText); 187 } 188 189 // save for other functions to use to determine if it is filled out. 190 paymentElementLastStatus = event; 191 }); 192 193 // checks if it is on the secondary payment complete page 194 if ($('#submit-new-payment').length) { 195 $('#submit-new-payment').slideDown('slow'); 196 } 197 198 // if message area exists, then show the payment plan messaging 199 if ($('.payment-plan-message').length) { 200 // messaging 201 showPaymentPlanMessage(); 202 } 203 204} 205 206async function unmountIntent(){ 207 // cancel the intent and remove its hidden fields 208 cancelPaymentIntent(); 209 $("#orderForm #OrderPaymentIntentId").remove(); 210 $("#orderForm #OrderPaymentIntentSecret").remove(); 211 212 // clear payment intent html 213 $("#payment-element").html(''); 214} 215 216// if it already has a payment intent but the total is changed or other things have impacted the calculate order method 217// we need to update the total on the payment intent instead of making a new one. 218async function updatePaymentIntent(){ 219 $.ajax({ 220 type: "POST", 221 url: buildURL("/orders/ajax_update_payment_intent"), 222 data: $("#orderForm").serialize(), 223 dataType: "json", 224 success: function (data) { 225 // this little chunk is needed to update the payment element 226 // that the payment intent has been updated so it can provide the correct information. 227 elements.fetchUpdates(); 228 // if message area exists, then show the payment plan messaging 229 if ($('.payment-plan-message').length) { 230 // messaging 231 showPaymentPlanMessage(); 232 } 233 } 234 }); 235} 236 237/** 238 * Displays a payment plan prompt under the order summary with the payment breakdown 239 */ 240function showPaymentPlanMessage() { 241 if(paymentPlanType == 'none'){ 242 // no need to add messaging but may want to add something in the future 243 } else { 244 // Get the text color for the page and use that on the payment element 245 var theme = 'stripe'; 246 var colorText = '#000000'; 247 var eventTheme = $('body').data('theme-color'); 248 249 if (eventTheme == 'dark') { 250 theme = 'night'; 251 colorText = '#ffffff'; 252 } 253 254 const appearance = { 255 theme: theme, 256 variables: { 257 fontFamily: 'Inter, Open Sans, sans-serif, system-ui', 258 colorText: colorText, 259 }, 260 rules: { 261 '.PaymentMethodMessaging': { 262 textAlign: 'center', 263 } 264 } 265 }; 266 267 const elements = stripe.elements({appearance}); 268 let proposedTotal = Math.round($('#OrderProposedTotal').val() * 100); 269 const options = { 270 amount: proposedTotal, 271 currency: currency, 272 paymentMethodTypes: [paymentPlanType], 273 countryCode: countryCode, 274 }; 275 276 const paymentPlanMessageElement = elements.create('paymentMethodMessaging', options); 277 paymentPlanMessageElement.mount('#payment-plan-message'); 278 279 const paymentPlanMessageElementMobile = elements.create('paymentMethodMessaging', options); 280 paymentPlanMessageElementMobile.mount('#payment-plan-message-mobile'); 281 282 $('#payment-plan-message').slideDown('fast'); 283 $('#payment-plan-message-mobile').slideDown('fast'); 284 285 } 286}
286 287 288 289/** 290 * handles the start of the payment process and processing of the funds 291 */ 292 async function handleSubmit() { 293 // Update a Payment Element after creation 294 const { error } = await stripe.confirmPayment({ // initiate and process the payment 295 elements, 296 clientSecret, 297 confirmParams: { 298 // Make sure to change this to your payment completion page 299 return_url: encodeURI("https://" + document.location.host + buildURL("/e/" + orderData.event_slug + "/payment/" + orderData.order_id + "/" + orderData.lang)), 300 }, 301 redirect: "if_required", 302 }); 303 let data = new Array(); 304 if (error) { 305 switch (error.type) { 306 case "api_error": // request error 307 case "api_connection_error": // issue during submission, hit back in browser, etc 308 stripe.retrievePaymentIntent(clientSecret).then(function (result) { 309 if (result.paymentIntent && result.paymentIntent.status && result.paymentIntent.status === 'succeeded') { 310 handlePaymentIntentSucceeded(result, orderData.order_id); 311 } else { 312 data.reason = "There was a problem processing your payment and your cart has expired. Please refresh the page to restart your order. [tpsjs"+ getLineNumber() + "]"; 313 data.code = "tpsjs" + getLineNumber(); 314 data.clearPrefill = true; 315 data.refresh = true; 316 submittingAlert.close(); 317 alertAndClose(data); 318 } 319 }); 320 break; 321 case "invalid_request_error": // payment processing failed. 322 if(error.payment_intent.status == 'canceled'){ 323 data.reason = "There was a problem processing your payment and your cart has expired. Please refresh the page to restart your order. [tpsjs"+ getLineNumber() + "]"; 324 data.code = "tpsjs" + getLineNumber(); 325 data.clearPrefill = true; 326 data.refresh = true; 327 submittingAlert.close(); 328 alertAndClose(data); 329 break; 330 } 331 case "card_error": // declined for funds or card status 332 case "validation_error": // for things like invalid CC number 333 default: 334 submittingAlert.close(); 335 data.reason = error.message; 336 data.refresh = false; 337 alertAndClose(data); 338 $('#payment-response').text(error.message).addClass('error'); //Sets in-line payment response banner 339 ignoreLeaveAttempts(); 340 orderSubmissionInProgress = false; 341 enableSubmit(origSubmitButtonText); 342 stripe.retrievePaymentIntent(clientSecret).then(function (result) { 343 if (result.paymentIntent.status != 'succeeded') { 344 handlePaymentIntentFailure(orderData.order_id, '_confirm_payment_' + error.type + '_' + error.code + '_' + getLineNumber()); 345 } 346 }); 347 break; 348 } 349 } else { 350 // get the current status of the intent 351 stripe.retrievePaymentIntent(clientSecret).then(function (result) { 352 if (result.paymentIntent.status == 'succeeded') { 353 handlePaymentIntentSucceeded(result, orderData.order_id); 354 } 355 }); 356 } 357} 358 359/** 360 * 361 * @param {*} clientSecret 362 * @param {*} orderId 363 */ 364function completePaymentIntent(clientSecret, orderId) { 365 const maxRetry = 5; 366 let retryCount = 0; 367 368 const executePaymentIntent = async () => { 369 try { 370 const result = await stripe.retrievePaymentIntent(clientSecret); 371 switch (result.paymentIntent.status) { 372 case "processing": 373 // TODO: handle checking again or placing this order is a pool to check later. 374 break; 375 case "succeeded": 376 // payment was successful 377 handlePaymentIntentSucceeded(result, orderId); 378 break; 379 case "amount_capturable_updated": 380 // TODO: handle rerunning the capture process for this charge 381 break; 382 case "requires_source": 383 case "canceled": 384 case "payment_failed": 385 default: 386 387 // payment was not successful 388 handlePaymentIntentFailure(orderId, '_completePaymentIntent_' + result.paymentIntent.status + '_' + getLineNumber()); 389 const data = { 390 redirect: true, 391 redirect_url: buildURL("/e/" + eventSlug + "/tickets/"), 392 code: 'tpsjs' + getLineNumber(), 393 clearPrefill: true, 394 reason: 'There was a problem processing your payment and your cart has expired. Please restart your order. [tpsjs' + getLineNumber() + ']' 395 }; 396 alertAndClose(data); // Show an alert and close the window 397 break; 398 } 399 } catch (error) { 400 // 400 Bad Request The request was unacceptable, often due to missing a required parameter. 401 // 401 Unauthorized No valid API key provided. 402 // 403 Forbidden The API key doesnât have permissions to perform the request. 403 404 // 402 Request Failed The parameters were valid but the request failed. 405 // 404 Not Found The requested resource doesnât exist. 406 // 409 Conflict The request conflicts with another request (perhaps due to using the same idempotent key). 407 // 429 Too Many Requests Too many requests hit the API too quickly. We recommend an exponential backoff of your requests. 408 // 500, 502, 503, 504 Server Errors Something went wrong on Stripeâs end. 409 const tryAgainStatusCodes = [402, 404, 409, 429, 500, 502, 503]; 410 const endItStatusCodes = [400, 401, 403]; 411 if (tryAgainStatusCodes.includes(error.statusCode) && retryCount < maxRetry) { 412 retryCount++; 413 setTimeout(executePaymentIntent, retryCount * 100); 414 } else if (endItStatusCodes.includes(error.statusCode) || retryCount >= maxRetry) { 415 handlePaymentIntentFailure(orderId, 'completePaymentIntent_catch_error_' + getLineNumber()); 416 const data = { 417 redirect: true, 418 redirect_url: buildURL("/e/" + eventSlug + "/tickets/"), 419 code: 'tpsjs' + getLineNumber(), 420 clearPrefill: true, 421 reason: 'There was a problem processing your payment and your cart has expired. Please restart your order. [tpsjs' + getLineNumber() + ']' 422 }; 423 alertAndClose(data); 424 } 425 } 426 }; 427 428 executePaymentIntent(); 429} 430 431/** 432 * 433 * @param {*} paymentInformation 434 * @param {*} orderId 435 */ 436function handlePaymentIntentSucceeded(paymentInformation, orderId) { 437 // Gather all order form data 438 var orderFormData = $('#orderForm').serializeArray(); 439 440 // Convert the order form data into an object 441 var orderDataObj = {}; 442 $.each(orderFormData, function (index, element) { 443 orderDataObj[element.name] = element.value; 444 }); 445 446 // Merge the payment information with the order data object 447 var dataToSend = $.extend({}, paymentInformation, orderDataObj); 448 449 $.ajax({ 450 type: "POST", 451 url: buildURL("/orders/ajax_save_completed_payment_intent_information/" + orderId), 452 data: dataToSend, 453 dataType: "json", 454 success: function (data) { 455 if (data.status == 'fail') { 456 data.code = "tpsjs" + getLineNumber(); 457 data.clearPrefill = true; 458 data.refresh = true; 459 submittingAlert.close(); 460 alertAndClose(data); 461 return; 462 } 463 // get redirect information from the initial order submission. 464 if (typeof orderData !== 'undefined') { 465 data.redirect = orderData.redirect; 466 } 467 ignoreLeaveAttempts(); 468 handlePaymentRedirect(data); 469 } 470 }); 471} 472 473/** 474 * Records a failed payment on the order and resets the checkout form so the buyer can try again. 475 * 476 * @param {string} orderId 477 * @param {string} lineInformation Tag identifying where the failure was detected, saved with the order 478 */ 479function handlePaymentIntentFailure(orderId, lineInformation) { 480 orderSubmissionInProgress = false;
481 clearOutSeatedFields(); 482 $.ajax({ 483 type: "POST", 484 url: buildURL("/orders/ajax_save_payment_failed_information/" + orderId + "/tpsjs" + lineInformation), 485 data: $("#orderForm").serialize(), 486 dataType: "json", 487 success: function (data) { 488 $('#OrderId').remove(); 489 enableSubmit(origSubmitButtonText); 490 return false; 491 }, 492 failed: function (data) { 493 $('#OrderId').remove(); 494 enableSubmit(origSubmitButtonText); 495 return false; 496 } 497 }); 498} 499 500/** 501 * 502 * @param {*} type 503 * @returns 504 */ 505function formatOrderData(type) { 506 //gather the name and billing address (if exists) to pass to the function 507 firstName = ($('#OrderFirstName').val() == 'undefined' ? '' : $('#OrderFirstName').val()); 508 lastName = ($('#OrderFirstName').val() == 'undefined' ? '' : $('#OrderLastName').val()); 509 billName = firstName + ' ' + lastName; 510 billName = $.trim(billName); 511 address1 = ($('#OrderAddress').val() == 'undefined' ? '' : $('#OrderAddress').val()); 512 address2 = ($('#OrderAddress2').val() == 'undefined' ? '' : $('#OrderAddress2').val()); 513 city = ($('#OrderCity').val() == 'undefined' ? '' : $('#OrderCity').val()); 514 postalCode = ($('#OrderPostalCode').val() == 'undefined' ? '' : $('#OrderPostalCode').val()); 515 country = ($('#OrderCountry').val() == 'undefined' ? '' : $('#OrderOrderCountry').val()); 516 517 //special handling for state, since there's 4 possible entry fields 518 if ($('#OrderState').val() != 'undefined' && $('#OrderState').val() != '') { 519 state = $('#OrderState').val(); 520 } else if ($('#OrderProvince').val() != 'undefined' && $('#OrderProvince').val() != '') { 521 state = $('#OrderProvince').val(); 522 } else if ($('#OrderAustralianState').val() != 'undefined' && $('#OrderAustralianState').val() != '') { 523 state = $('#OrderAustralianState').val(); 524 } else if ($('#OrderMxDistrict').val() != 'undefined' && $('#OrderMxDistrict').val() != '') { 525 state = $('#OrderMxDistrict').val(); 526 } else { 527 state = ''; 528 } 529 530 return { 531 payment_method_data: { 532 billing_details: { 533 name: billName, 534 address: { 535 city: city, 536 country: country, 537 line1: address1, 538 line2: address2, 539 postal_code: postalCode, 540 state: state, 541 542 }, 543 }, 544 shipping: { 545 name: billName, 546 address: { 547 city: city, 548 country: country, 549 line1: address1, 550 line2: address2, 551 postal_code: postalCode, 552 state: state, 553 554 } 555 } 556 } 557 }; 558} 559 560/** 561 * Get the line number of the current execution point. 562 * 563 * This function retrieves the line number of the current execution point by parsing the call stack. 564 * It creates a new Error object to capture the stack trace, then extracts the line number from the stack trace. 565 * The line number is obtained by splitting the stack trace and using regular expression matching. 566 * 567 * @return int|string The line number of the current execution point if found, otherwise 'unknown'. 568 * @throws \Exception If an error occurs while retrieving the line number. 569 */ 570function getLineNumber() { 571 try { 572 throw new Error(); 573 } catch (e) { 574 if (!e.stack) { 575 return 'unknown'; 576 } 577 const stack = e.stack.split("\n"); 578 const callerLine = stack[2] || stack[1]; 579 const match = callerLine.match(/:(\d+):\d+/); // Regular expression to extract line number 580 581 return match ? match[1] : 'unknown'; 582 } 583}
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.