1(function ($) { 2 $(document).ready(function() { 3 $('.mepr-signup-form, #mepr-stripe-payment-form').each(function () { 4 new MeprStripeForm(this); 5 }); 6 }); 7 8 /** 9 * The MemberPress Stripe form class 10 * 11 * @constructor 12 * @param {HTMLFormElement} form 13 */ 14 function MeprStripeForm (form) { 15 this.form = form; 16 this.$form = $(form); 17 this.isSpc = this.$form.hasClass('mepr-signup-form'); 18 this.paymentMethods = []; 19 this.selectedPaymentMethod = null; 20 this.submitting = false; 21 this.updateBillingDetailsTimeout = null; 22 this.initPaymentMethods(); 23 this.$form.on('submit', $.proxy(this.handleSubmit, this)); 24 this.$form.on('meprAfterCheckoutStateUpdated', $.proxy(this.updateElements, this)); 25 this.$form.find('input[name="user_email"]').on('change', $.proxy(this.updateBillingDetails, this)); 26 } 27 28 /** 29 * Initialize Stripe elements 30 */ 31 MeprStripeForm.prototype.initPaymentMethods = function () { 32 var self = this, 33 templateOutdated = false; 34 35 self.$form.find('.mepr-stripe-card-element').each(function () { 36 var $cardElement = $(this), 37 $cardErrors = $cardElement.closest('.mepr-stripe-elements').find('.mepr-stripe-card-errors'), 38 stripe = Stripe($cardElement.data('stripe-public-key'), { 39 locale: $cardElement.data('locale-code').toLowerCase(), 40 apiVersion: MeprStripeGateway.api_version 41 }), 42 paymentMethodId = $cardElement.data('payment-method-id'), 43 elementsOptions = $cardElement.data('elements-options'), 44 wrapperSelector = self.isSpc ? '.mepr-payment-method' : '.mp_payment_form_wrapper', 45 $wrapper = $cardElement.closest(wrapperSelector), 46 paymentMethod = { 47 id: paymentMethodId, 48 $cardElement: $cardElement, 49 $cardErrors: $cardErrors, 50 $wrapper: $wrapper, 51 stripe: stripe, 52 elements: null, 53 paymentElement: null, 54 paymentElementComplete: false 55 }; 56 57 self.paymentMethods.push(paymentMethod); 58 59 if (elementsOptions) { 60 self.createElements(paymentMethod, elementsOptions); 61 } else if (self.isSpc) { 62 templateOutdated = true; 63 } 64 }); 65 66 if (templateOutdated) { 67 self.$form.trigger('meprUpdateCheckoutState'); 68 console.log('Your MemberPress form template is out of date, please update it to the latest version.'); 69 } 70 }; 71 72 /** 73 * Create an instance of Elements and a Payment Element 74 * 75 * @param {object} paymentMethod 76 * @param options 77 */ 78 MeprStripeForm.prototype.createElements = function (paymentMethod, options) { 79 try { 80 var self = this, 81 applePayConfig = self.processApplePayConfig(options.applePay || null); 82 83 delete options.applePay; 84 85 // Snapshot the server-provided options for the submit-time staleness check, 86 // before the client-side Elements additions below. 87 paymentMethod.appliedElementsContext = JSON.stringify(options); 88 89 options = $.extend({ 90 currency: MeprStripeGateway.currency, 91 appearance: MeprStripeGateway.elements_appearance 92 }, options); 93 94 paymentMethod.elements = paymentMethod.stripe.elements(options); 95 paymentMethod.applePayConfig = applePayConfig; 96 97 var paymentElementOptions = { 98 defaultValues: { 99 billingDetails: self.getBillingDetails(paymentMethod) 100 }, 101 terms: MeprStripeGateway.payment_element_terms 102 }; 103 104 if (applePayConfig) { 105 paymentElementOptions.applePay = applePayConfig; 106 } 107 108 paymentMethod.paymentElement = paymentMethod.elements.create('payment', paymentElementOptions); 109 110 self.attachPaymentElementHandlers(paymentMethod); 111 112 paymentMethod.paymentElement.mount(paymentMethod.$cardElement[0]); 113 } catch (e) { 114 paymentMethod.$cardErrors.html(e.message); 115 throw e; 116 } 117 }; 118 119 /** 120 * Process an Apple Pay config from the server, converting trialDays to a Date for regularBilling.recurringPaymentStartDate. 121 * 122 * @param {object|null} applePayConfig The Apple Pay config from the server. 123 * @return {object|null} The processed config ready for Stripe. 124 */ 125 MeprStripeForm.prototype.processApplePayConfig = function (applePayConfig) { 126 if (!applePayConfig || !applePayConfig.recurringPaymentRequest) { 127 return null; 128 } 129 130 var request = applePayConfig.recurringPaymentRequest; 131 132 // Stripe's Payment Element handles paymentTiming internally and their docs don't include it. 133 if (request.regularBilling) { 134 delete request.regularBilling.paymentTiming; 135 } 136 137 if (request.trialDays) { 138 if (request.regularBilling) { 139 var trialEndDate = new Date(); 140 141 trialEndDate.setDate(trialEndDate.getDate() + request.trialDays); 142 trialEndDate.setHours(0, 0, 0, 0); 143 144 request.regularBilling.recurringPaymentStartDate = trialEndDate; 145 } 146 147 delete request.trialDays; 148 } 149 150 return applePayConfig; 151 }; 152 153 /** 154 * Attach event handlers to a payment element. 155 * 156 * @param {object} paymentMethod The payment method object. 157 */ 158 MeprStripeForm.prototype.attachPaymentElementHandlers = function (paymentMethod) { 159 paymentMethod.paymentElement.on('loaderror', function (event) { 160 if (event.error) { 161 paymentMethod.$cardErrors.html(event.error.message || 'Failed to create Payment element'); 162 } 163 }); 164 165 paymentMethod.paymentElement.on('change', function (event) { 166 if (typeof event.complete === 'boolean') { 167 paymentMethod.paymentElementComplete = event.complete; 168 } 169 170 if (typeof event.value === 'object' && typeof event.value.type === 'string') { 171 paymentMethod.paymentMethodType = event.value.type; 172 } else { 173 paymentMethod.paymentMethodType = null; 174 } 175 }); 176 }; 177 178 /** 179 * Updates the Elements instance with new options 180 * 181 * @param {object} event 182 * @param {object} response 183 */ 184 MeprStripeForm.prototype.updateElements = function (event, response) { 185 var self = this; 186 187 if (response.payment_required && response.elements_options) { 188 $.each(response.elements_options, function (paymentMethodId, options) { 189 var paymentMethod = self.getPaymentMethodById(paymentMethodId); 190 191 if (paymentMethod) { 192 if (paymentMethod.elements) { 193 var newApplePayConfig = self.processApplePayConfig(options.applePay || null); 194 195 delete options.applePay; 196 197 paymentMethod.elements.update(options); 198 199 // Snapshot the applied options for the submit-time staleness check. 200 paymentMethod.appliedElementsContext = JSON.stringify(options); 201 202 // Destroy and recreate the payment element when the Apple Pay config needs
203 // to be removed, since paymentElement.update() uses a shallow merge and 204 // cannot unset an existing Apple Pay config. 205 if (paymentMethod.applePayConfig && !newApplePayConfig) { 206 paymentMethod.applePayConfig = null; 207 paymentMethod.paymentElement.destroy(); 208 209 paymentMethod.paymentElement = paymentMethod.elements.create('payment', { 210 defaultValues: { 211 billingDetails: self.getBillingDetails(paymentMethod) 212 }, 213 terms: MeprStripeGateway.payment_element_terms 214 }); 215 216 self.attachPaymentElementHandlers(paymentMethod); 217 218 paymentMethod.paymentElement.mount(paymentMethod.$cardElement[0]); 219 } else { 220 paymentMethod.applePayConfig = newApplePayConfig; 221 222 if (newApplePayConfig) { 223 paymentMethod.paymentElement.update({ applePay: newApplePayConfig }); 224 } 225 } 226 } else { 227 self.createElements(paymentMethod, options); 228 } 229 } 230 }); 231 } 232 233 self.updateBillingDetails(); 234 }; 235 236 /** 237 * Update the billing details on each Payment Element in this form 238 */ 239 MeprStripeForm.prototype.updateBillingDetails = function () { 240 var self = this; 241 242 clearTimeout(self.updateBillingDetailsTimeout); 243 244 self.updateBillingDetailsTimeout = setTimeout(function () { 245 for (var i = 0; i < self.paymentMethods.length; i++) { 246 var paymentMethod = self.paymentMethods[i]; 247 248 if (paymentMethod.paymentElement) { 249 paymentMethod.paymentElement.update({ 250 defaultValues: { 251 billingDetails: self.getBillingDetails(paymentMethod) 252 } 253 }); 254 } 255 } 256 }, 50); 257 }; 258 259 /** 260 * Handle the payment form submission 261 * 262 * @param {jQuery.Event} e 263 */ 264 MeprStripeForm.prototype.handleSubmit = async function (e) { 265 var self = this; 266 267 if (self.$form.find('.mepr-payment-methods-wrapper').is(':hidden')) { 268 return; 269 } 270 271 self.selectedPaymentMethod = self.getSelectedPaymentMethod(); 272 273 if (self.selectedPaymentMethod) { 274 e.preventDefault(); 275 276 // Wait for an in-flight checkout state update, so the Payment Element is 277 // reconciled with the final order before confirming. 278 if (self.$form.data('meprCheckoutStateUpdating')) { 279 self.$form.data('meprSubmitWhenUpdated', true); 280 return; 281 } 282 283 if (!self.selectedPaymentMethod.paymentElementComplete) { 284 self.selectedPaymentMethod.$cardErrors.text(MeprStripeGateway.payment_information_incomplete); 285 self.allowResubmission(); 286 return; 287 } 288 289 if (self.submitting) { 290 return; 291 } 292 293 self.submitting = true; 294 self.$form.find('.mepr-form-has-errors').hide(); 295 self.$form.find('.mepr-submit').prop('disabled', true); 296 self.$form.find('.mepr-loading-gif').show(); 297 298 self.selectedPaymentMethod.$cardErrors.html(''); 299 300 const { error } = await self.selectedPaymentMethod.elements.submit(); 301 302 if (error) { 303 this.handlePaymentError(error.message); 304 return; 305 } 306 307 self.processForm(); 308 } 309 }; 310 311 /** 312 * Get the data for a payment method by ID 313 * 314 * @param {string} paymentMethodId 315 * @return {object|null} 316 */ 317 MeprStripeForm.prototype.getPaymentMethodById = function (paymentMethodId) { 318 for (var i = 0; i < this.paymentMethods.length; i++) { 319 if (this.paymentMethods[i].id === paymentMethodId) { 320 return this.paymentMethods[i]; 321 } 322 } 323 324 return null; 325 }; 326 327 /** 328 * Get the currently selected payment method data 329 * 330 * @return {object|null} 331 */ 332 MeprStripeForm.prototype.getSelectedPaymentMethod = function () { 333 if (this.isSpc) { 334 return this.getPaymentMethodById(this.$form.find('input[name="mepr_payment_method"]:checked').val()); 335 } else { 336 return this.paymentMethods.length ? this.paymentMethods[0] : null; 337 } 338 }; 339 340 /** 341 * Get the billing details object to pass to Stripe 342 * 343 * @param {object} paymentMethod The selected payment method. 344 * @return {object} 345 */ 346 MeprStripeForm.prototype.getBillingDetails = function (paymentMethod) { 347 var self = this, 348 keys = { 349 line1: 'mepr-address-one', 350 line2: 'mepr-address-two', 351 city: 'mepr-address-city', 352 country: 'mepr-address-country', 353 state: 'mepr-address-state', 354 postal_code: 'mepr-address-zip' 355 }, 356 email = '', 357 name = [], 358 address = {}, 359 addressFieldsPresent = false, 360 details = { 361 address: {} 362 }; 363 364 var $emailField = self.$form.find('input[name="user_email"]'); 365 366 if ($emailField.length) { 367 email = $emailField.val(); 368 } else { 369 email = paymentMethod.$cardElement.data('user-email'); 370 } 371 372 if (email && email.length) { 373 details.email = email; 374 } 375 376 $.each(['user_first_name', 'user_last_name'], function (index, value) { 377 var $field = self.$form.find('input[name="' + value + '"]'); 378 379 if ($field.length) { 380 var val = $field.val(); 381 382 if (typeof val == 'string' && val.length) { 383 name.push(val); 384 } 385 } 386 }); 387 388 if (name.length) { 389 details.name = name.join(' '); 390 } else { 391 $.each(['card-first-name', 'card-last-name'], function (index, value) { 392 var $field = paymentMethod.$wrapper.find('input[name="' + value + '"]'); 393 394 if ($field.length) { 395 var val = $field.val(); 396 397 if (typeof val == 'string' && val.length) { 398 name.push(val); 399 } 400 } 401 }); 402 403 if (name.length) { 404 details.name = name.join(' '); 405 } 406 } 407 408 $.each(keys, function (key, value) { 409 var $field = self.$form.find('input[name="' + value + '"], select[name="' + value + '"]'); 410 411 if ($field.length) { 412 var val = $field.val(); 413 414 if (typeof val == 'string' && val.length) { 415 address[key] = val; 416 } 417 418 addressFieldsPresent = true; 419 } 420 }); 421 422 if (addressFieldsPresent) { 423 details.address = address; 424 } else { 425 $.each(keys, function (key, value) { 426 var cardAddressKey = value.replace('mepr-', 'card-'), 427 $field = paymentMethod.$wrapper.find('input[name="' + cardAddressKey + '"]'); 428 429 if ($field.length) { 430 var val = $field.val(); 431 432 if (typeof val == 'string' && val.length) { 433 details.address[key] = val; 434 } 435 } 436 }); 437 } 438 439 return details; 440 }; 441 442 /** 443 * Allow the form to be submitted again 444 */ 445 MeprStripeForm.prototype.allowResubmission = function () { 446 this.submitting = false;
447 this.$form.find('.mepr-submit').prop('disabled', false); 448 this.$form.find('.mepr-loading-gif').hide(); 449 this.$form.find('.mepr-form-has-errors').show(); 450 this.$form.find('.mepr-validation-error, .mepr-top-error').remove(); 451 }; 452 453 /** 454 * Handle form validation errors 455 * 456 * @param {array} errors The validation errors array 457 */ 458 MeprStripeForm.prototype.handleValidationErrors = function (errors) { 459 // Allow the form to be submitted again 460 this.allowResubmission(); 461 462 var topErrors = []; 463 464 for (var key in errors) { 465 if (errors.hasOwnProperty(key)) { 466 var $field = this.$form.find('[name="' + key + '"]').first(), 467 $label = $field.closest('.mp-form-row').find('.mp-form-label'); 468 469 if ($.isNumeric(key) || !$label.length) { 470 topErrors.push(errors[key]); 471 } else { 472 $label.append($('<span class="mepr-validation-error">').html(errors[key])); 473 } 474 } 475 } 476 477 if (topErrors.length) { 478 var $list = $('<ul>'), 479 $wrap = $('<div class="mepr-top-error mepr_error">'); 480 481 for (var i = 0; i < topErrors.length; i++) { 482 $list.append($('<li>').html(MeprStripeGateway.top_error.replace('%s', topErrors[i]))); 483 } 484 485 $wrap.append($list).prependTo(this.$form); 486 } 487 }; 488 489 /** 490 * Handle an error with the payment 491 * 492 * @param {string} error The error message to display 493 */ 494 MeprStripeForm.prototype.handlePaymentError = function (error) { 495 // Allow the form to be submitted again 496 this.allowResubmission(); 497 498 // Inform the user if there was an error 499 this.selectedPaymentMethod.$cardErrors.html(error); 500 }; 501 502 /** 503 * Handle any SCA actions, and confirms the payment or card setup 504 * 505 * @param {string} action 506 * @param {string} clientSecret 507 * @param {string} returnUrl 508 */ 509 MeprStripeForm.prototype.handleAction = async function (action, clientSecret, returnUrl) { 510 var self = this, 511 stripe = self.selectedPaymentMethod.stripe, 512 elements = self.selectedPaymentMethod.elements, 513 billingDetails = self.getBillingDetails(self.selectedPaymentMethod), 514 confirmParams = { 515 return_url: returnUrl, 516 payment_method_data: { 517 billing_details: billingDetails 518 } 519 }; 520 521 if (self.selectedPaymentMethod.paymentMethodType === 'affirm') { 522 // Affirm seems to have issues if the billing address is provided 523 delete confirmParams.payment_method_data; 524 } 525 526 if (self.selectedPaymentMethod.paymentMethodType === 'afterpay_clearpay') { 527 // Afterpay/Clearpay require a shipping address 528 confirmParams.shipping = { 529 name: billingDetails.name || '', 530 address: billingDetails.address 531 }; 532 } 533 534 const { error } = await stripe[action]({ 535 elements: elements, 536 clientSecret: clientSecret, 537 confirmParams: confirmParams 538 }); 539 540 if (error) { 541 self.handlePaymentError(error.message); 542 } 543 }; 544 545 /** 546 * Process the form. 547 */ 548 MeprStripeForm.prototype.processForm = function () { 549 var self = this, 550 formData = new FormData(self.form); 551 552 formData.append('action', 'mepr_process_' + (self.isSpc ? 'signup' : 'payment') + '_form'); 553 554 // We don't want to hit our routes for processing the signup or payment forms 555 formData.delete('mepr_process_signup_form'); 556 formData.delete('mepr_process_payment_form'); 557 558 // Report the applied Elements options so the server can detect a stale 559 // checkout. The field is absent when the Elements were never created (for 560 // example, outdated form templates or cached scripts), in which case the 561 // server skips the reconciliation. The standalone payment form never reports 562 // a context: its render and submit inputs are the same stored transaction 563 // state, so the contexts cannot diverge there. 564 if (self.isSpc && self.selectedPaymentMethod.appliedElementsContext) { 565 formData.append('mepr_stripe_elements_context', self.selectedPaymentMethod.appliedElementsContext); 566 } 567 568 $.ajax({ 569 type: 'POST', 570 url: MeprStripeGateway.ajax_url, 571 data: formData, 572 dataType: 'json', 573 cache: false, 574 processData: false, 575 contentType: false, 576 headers: { 577 'cache-control': 'no-cache' 578 } 579 }) 580 .done(function (response, textStatus, jqXHR) { 581 if (response === null || typeof response !== 'object' || typeof response.success !== 'boolean') { 582 self.handlePaymentError(MeprStripeGateway.invalid_response_error); 583 self.debugCheckoutError({ 584 status: jqXHR.status, 585 status_text: jqXHR.statusText, 586 response_text: jqXHR.responseText, 587 text_status: textStatus, 588 error_thrown: 'Response was null or not an object' 589 }); 590 } else { 591 if (response.success) { 592 self.handleAction(response.data.action, response.data.client_secret, response.data.return_url); 593 } else { 594 if (response.data && response.data.checkout_state_changed) { 595 // Force a checkout-state refresh, which updates the invoice, the 596 // price string, and the Payment Element together, so the user can 597 // re-confirm the payment with the correct terms.
598 self.$form.trigger('meprUpdateCheckoutState', [true]); 599 self.handlePaymentError(MeprStripeGateway.checkout_state_changed); 600 } else if (response.data.errors) { 601 self.handleValidationErrors(response.data.errors); 602 } else { 603 self.handlePaymentError(response.data); 604 } 605 } 606 } 607 }) 608 .fail(function (jqXHR, textStatus, errorThrown) { 609 if (jqXHR.status === 0) { 610 // Don't send a debug email for errors with status 0 611 self.handlePaymentError(MeprStripeGateway.error_please_try_again); 612 } else { 613 self.handlePaymentError(MeprStripeGateway.ajax_error); 614 self.debugCheckoutError({ 615 status: jqXHR.status, 616 status_text: jqXHR.statusText, 617 response_text: jqXHR.responseText, 618 text_status: textStatus, 619 error_thrown: '' + errorThrown 620 }); 621 } 622 }); 623 }; 624 625 /** 626 * Debug a critical checkout error 627 * 628 * @param {object} data 629 */ 630 MeprStripeForm.prototype.debugCheckoutError = function (data) { 631 data.transaction_id = this.$form.find('input[name="mepr_transaction_id"]').val(); 632 data.customer_email = this.$form.find('input[name="user_email"]').val(); 633 data.checkout_token = this.$form.find('input[name="mepr_checkout_token"]').val(); 634 635 $.ajax({ 636 type: 'POST', 637 url: MeprStripeGateway.ajax_url, 638 dataType: 'json', 639 data: { 640 action: 'mepr_debug_checkout_error', 641 data: JSON.stringify(data) 642 } 643 }); 644 }; 645})(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.