WooCommerce Stripe Payment Gateway - Version 4.3.0

Version Description

2019-10-17 = * Add - For WooCommerce Subscriptions optimize the payment flow for subsequent subscription payments when authentication may be required by using the setup_future_usage parameter for the first subscription payment * Add - Allow customer to authenticate payment even if they are not charged right away for WooCommerce Subscriptions and Pre-Orders, for example for a WooCommerce Subscription that has a free trial * Add - When an off-session payment requires authentication, create a link for customers to come back to the store to authenticate the payment * Add - Send an email to WooCommerce Subscription and Pre-Orders customers who need to authenticate a payment that was automatically tried on their behalf * Add - When an off-session payment requires authentication, send an email to the admin * Add - Admin notice about SCA-readiness * Fix - Avoid idempotency key errors for Pre-Orders * Fix - Use unique anchor for link about checkout styling changes

Download this release

Release Info

Developer woothemes
Plugin Icon 128x128 WooCommerce Stripe Payment Gateway
Version 4.3.0
Comparing to
See all releases

Code changes from version 4.2.5 to 4.3.0

assets/js/stripe.js CHANGED
@@ -709,30 +709,32 @@ jQuery( function( $ ) {
709
  },
710
 
711
  /**
712
- * Handles changes in the hash in order to show a modal for PaymentIntent confirmations.
713
  *
714
  * Listens for `hashchange` events and checks for a hash in the following format:
715
  * #confirm-pi-<intentClientSecret>:<successRedirectURL>
716
  *
717
  * If such a hash appears, the partials will be used to call `stripe.handleCardPayment`
718
- * in order to allow customers to confirm an 3DS/SCA authorization.
 
719
  *
720
  * Those redirects/hashes are generated in `WC_Gateway_Stripe::process_payment`.
721
  */
722
  onHashChange: function() {
723
- var partials = window.location.hash.match( /^#?confirm-pi-([^:]+):(.+)$/ );
724
 
725
- if ( ! partials || 3 > partials.length ) {
726
  return;
727
  }
728
 
729
- var intentClientSecret = partials[1];
730
- var redirectURL = decodeURIComponent( partials[2] );
 
731
 
732
  // Cleanup the URL
733
  window.location.hash = '';
734
 
735
- wc_stripe_form.openIntentModal( intentClientSecret, redirectURL );
736
  },
737
 
738
  maybeConfirmIntent: function() {
@@ -743,7 +745,7 @@ jQuery( function( $ ) {
743
  var intentSecret = $( '#stripe-intent-id' ).val();
744
  var returnURL = $( '#stripe-intent-return' ).val();
745
 
746
- wc_stripe_form.openIntentModal( intentSecret, returnURL, true );
747
  },
748
 
749
  /**
@@ -753,15 +755,18 @@ jQuery( function( $ ) {
753
  * @param {string} redirectURL The URL to ping on fail or redirect to on success.
754
  * @param {boolean} alwaysRedirect If set to true, an immediate redirect will happen no matter the result.
755
  * If not, an error will be displayed on failure.
 
 
756
  */
757
- openIntentModal: function( intentClientSecret, redirectURL, alwaysRedirect ) {
758
- stripe.handleCardPayment( intentClientSecret )
759
  .then( function( response ) {
760
  if ( response.error ) {
761
  throw response.error;
762
  }
763
 
764
- if ( 'requires_capture' !== response.paymentIntent.status && 'succeeded' !== response.paymentIntent.status ) {
 
765
  return;
766
  }
767
 
709
  },
710
 
711
  /**
712
+ * Handles changes in the hash in order to show a modal for PaymentIntent/SetupIntent confirmations.
713
  *
714
  * Listens for `hashchange` events and checks for a hash in the following format:
715
  * #confirm-pi-<intentClientSecret>:<successRedirectURL>
716
  *
717
  * If such a hash appears, the partials will be used to call `stripe.handleCardPayment`
718
+ * in order to allow customers to confirm an 3DS/SCA authorization, or stripe.handleCardSetup if
719
+ * what needs to be confirmed is a SetupIntent.
720
  *
721
  * Those redirects/hashes are generated in `WC_Gateway_Stripe::process_payment`.
722
  */
723
  onHashChange: function() {
724
+ var partials = window.location.hash.match( /^#?confirm-(pi|si)-([^:]+):(.+)$/ );
725
 
726
+ if ( ! partials || 4 > partials.length ) {
727
  return;
728
  }
729
 
730
+ var type = partials[1];
731
+ var intentClientSecret = partials[2];
732
+ var redirectURL = decodeURIComponent( partials[3] );
733
 
734
  // Cleanup the URL
735
  window.location.hash = '';
736
 
737
+ wc_stripe_form.openIntentModal( intentClientSecret, redirectURL, false, 'si' === type );
738
  },
739
 
740
  maybeConfirmIntent: function() {
745
  var intentSecret = $( '#stripe-intent-id' ).val();
746
  var returnURL = $( '#stripe-intent-return' ).val();
747
 
748
+ wc_stripe_form.openIntentModal( intentSecret, returnURL, true, false );
749
  },
750
 
751
  /**
755
  * @param {string} redirectURL The URL to ping on fail or redirect to on success.
756
  * @param {boolean} alwaysRedirect If set to true, an immediate redirect will happen no matter the result.
757
  * If not, an error will be displayed on failure.
758
+ * @param {boolean} isSetupIntent If set to true, ameans that the flow is handling a Setup Intent.
759
+ * If false, it's a Payment Intent.
760
  */
761
+ openIntentModal: function( intentClientSecret, redirectURL, alwaysRedirect, isSetupIntent ) {
762
+ stripe[ isSetupIntent ? 'handleCardSetup' : 'handleCardPayment' ]( intentClientSecret )
763
  .then( function( response ) {
764
  if ( response.error ) {
765
  throw response.error;
766
  }
767
 
768
+ var intent = response[ isSetupIntent ? 'setupIntent' : 'paymentIntent' ];
769
+ if ( 'requires_capture' !== intent.status && 'succeeded' !== intent.status ) {
770
  return;
771
  }
772
 
assets/js/stripe.min.js CHANGED
@@ -1 +1 @@
1
- jQuery(function(c){"use strict";try{var n=Stripe(wc_stripe_params.key)}catch(e){return void console.log(e)}var t,o,i,e=Object.keys(wc_stripe_params.elements_options).length?wc_stripe_params.elements_options:{},r=Object.keys(wc_stripe_params.sepa_elements_options).length?wc_stripe_params.sepa_elements_options:{},s=n.elements(e),a=s.create("iban",r),m={getAjaxURL:function(e){return wc_stripe_params.ajaxurl.toString().replace("%%endpoint%%","wc_stripe_"+e)},unmountElements:function(){"yes"===wc_stripe_params.inline_cc_form?t.unmount("#stripe-card-element"):(t.unmount("#stripe-card-element"),o.unmount("#stripe-exp-element"),i.unmount("#stripe-cvc-element"))},mountElements:function(){if(c("#stripe-card-element").length){if("yes"===wc_stripe_params.inline_cc_form)return t.mount("#stripe-card-element");t.mount("#stripe-card-element"),o.mount("#stripe-exp-element"),i.mount("#stripe-cvc-element")}},createElements:function(){var e={base:{iconColor:"#666EE8",color:"#31325F",fontSize:"15px","::placeholder":{color:"#CFD7E0"}}},r={focus:"focused",empty:"empty",invalid:"invalid"};e=wc_stripe_params.elements_styling?wc_stripe_params.elements_styling:e,r=wc_stripe_params.elements_classes?wc_stripe_params.elements_classes:r,"yes"===wc_stripe_params.inline_cc_form?(t=s.create("card",{style:e,hidePostalCode:!0})).addEventListener("change",function(e){m.onCCFormChange(),e.error&&c(document.body).trigger("stripeError",e)}):(t=s.create("cardNumber",{style:e,classes:r}),o=s.create("cardExpiry",{style:e,classes:r}),i=s.create("cardCvc",{style:e,classes:r}),t.addEventListener("change",function(e){m.onCCFormChange(),m.updateCardBrand(e.brand),e.error&&c(document.body).trigger("stripeError",e)}),o.addEventListener("change",function(e){m.onCCFormChange(),e.error&&c(document.body).trigger("stripeError",e)}),i.addEventListener("change",function(e){m.onCCFormChange(),e.error&&c(document.body).trigger("stripeError",e)})),"yes"===wc_stripe_params.is_checkout?c(document.body).on("updated_checkout",function(){t&&m.unmountElements(),m.mountElements(),c("#stripe-iban-element").length&&a.mount("#stripe-iban-element")}):(c("form#add_payment_method").length||c("form#order_review").length)&&(m.mountElements(),c("#stripe-iban-element").length&&a.mount("#stripe-iban-element"))},updateCardBrand:function(e){var r={visa:"stripe-visa-brand",mastercard:"stripe-mastercard-brand",amex:"stripe-amex-brand",discover:"stripe-discover-brand",diners:"stripe-diners-brand",jcb:"stripe-jcb-brand",unknown:"stripe-credit-card-brand"},t=c(".stripe-card-brand"),n="stripe-credit-card-brand";e in r&&(n=r[e]),c.each(r,function(e,r){t.removeClass(r)}),t.addClass(n)},init:function(){"yes"!==wc_stripe_params.is_change_payment_page&&"yes"!==wc_stripe_params.is_pay_for_order_page||c(document.body).trigger("wc-credit-card-form-init"),c("form.woocommerce-checkout").length&&(this.form=c("form.woocommerce-checkout")),c("form.woocommerce-checkout").on("checkout_place_order_stripe checkout_place_order_stripe_bancontact checkout_place_order_stripe_sofort checkout_place_order_stripe_giropay checkout_place_order_stripe_ideal checkout_place_order_stripe_alipay checkout_place_order_stripe_sepa",this.onSubmit),c("form#order_review").length&&(this.form=c("form#order_review")),c("form#order_review, form#add_payment_method").on("submit",this.onSubmit),c("form#add_payment_method").length&&(this.form=c("form#add_payment_method")),c("form.woocommerce-checkout").on("change",this.reset),c(document).on("stripeError",this.onError).on("checkout_error",this.reset),a.on("change",this.onSepaError),m.createElements(),window.addEventListener("hashchange",m.onHashChange),m.maybeConfirmIntent()},isStripeChosen:function(){return c("#payment_method_stripe, #payment_method_stripe_bancontact, #payment_method_stripe_sofort, #payment_method_stripe_giropay, #payment_method_stripe_ideal, #payment_method_stripe_alipay, #payment_method_stripe_sepa, #payment_method_stripe_eps, #payment_method_stripe_multibanco").is(":checked")||c("#payment_method_stripe").is(":checked")&&"new"===c('input[name="wc-stripe-payment-token"]:checked').val()||c("#payment_method_stripe_sepa").is(":checked")&&"new"===c('input[name="wc-stripe-payment-token"]:checked').val()},isStripeSaveCardChosen:function(){return c("#payment_method_stripe").is(":checked")&&c('input[name="wc-stripe-payment-token"]').is(":checked")&&"new"!==c('input[name="wc-stripe-payment-token"]:checked').val()||c("#payment_method_stripe_sepa").is(":checked")&&c('input[name="wc-stripe_sepa-payment-token"]').is(":checked")&&"new"!==c('input[name="wc-stripe_sepa-payment-token"]:checked').val()},isStripeCardChosen:function(){return c("#payment_method_stripe").is(":checked")},isBancontactChosen:function(){return c("#payment_method_stripe_bancontact").is(":checked")},isGiropayChosen:function(){return c("#payment_method_stripe_giropay").is(":checked")},isIdealChosen:function(){return c("#payment_method_stripe_ideal").is(":checked")},isSofortChosen:function(){return c("#payment_method_stripe_sofort").is(":checked")},isAlipayChosen:function(){return c("#payment_method_stripe_alipay").is(":checked")},isSepaChosen:function(){return c("#payment_method_stripe_sepa").is(":checked")},isP24Chosen:function(){return c("#payment_method_stripe_p24").is(":checked")},isEpsChosen:function(){return c("#payment_method_stripe_eps").is(":checked")},isMultibancoChosen:function(){return c("#payment_method_stripe_multibanco").is(":checked")},hasSource:function(){return 0<c("input.stripe-source").length},isMobile:function(){return!!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)},block:function(){m.isMobile()||m.form.block({message:null,overlayCSS:{background:"#fff",opacity:.6}})},unblock:function(){m.form&&m.form.unblock()},getSelectedPaymentElement:function(){return c('.payment_methods input[name="payment_method"]:checked')},getOwnerDetails:function(){var e=c("#billing_first_name").length?c("#billing_first_name").val():wc_stripe_params.billing_first_name,r=c("#billing_last_name").length?c("#billing_last_name").val():wc_stripe_params.billing_last_name,t={name:"",address:{},email:"",phone:""};return t.name=e,t.name=e&&r?e+" "+r:c("#stripe-payment-data").data("full-name"),t.email=c("#billing_email").val(),t.phone=c("#billing_phone").val(),(void 0===t.phone||t.phone.length<=0)&&delete t.phone,(void 0===t.email||t.email.length<=0)&&(c("#stripe-payment-data").data("email").length?t.email=c("#stripe-payment-data").data("email"):delete t.email),(void 0===t.name||t.name.length<=0)&&delete t.name,t.address.line1=c("#billing_address_1").val()||wc_stripe_params.billing_address_1,t.address.line2=c("#billing_address_2").val()||wc_stripe_params.billing_address_2,t.address.state=c("#billing_state").val()||wc_stripe_params.billing_state,t.address.city=c("#billing_city").val()||wc_stripe_params.billing_city,t.address.postal_code=c("#billing_postcode").val()||wc_stripe_params.billing_postcode,t.address.country=c("#billing_country").val()||wc_stripe_params.billing_country,{owner:t}},createSource:function(){var e=m.getOwnerDetails();return m.isSepaChosen()?(e.currency=c("#stripe-sepa_debit-payment-data").data("currency"),e.mandate={notification_method:wc_stripe_params.sepa_mandate_notification},e.type="sepa_debit",n.createSource(a,e).then(m.sourceResponse)):n.createSource(t,e).then(m.sourceResponse)},sourceResponse:function(e){if(e.error)return c(document.body).trigger("stripeError",e);m.reset(),m.form.append(c('<input type="hidden" />').addClass("stripe-source").attr("name","stripe_source").val(e.source.id)),c("form#add_payment_method").length&&c(m.form).off("submit",m.form.onSubmit),m.form.submit()},onSubmit:function(){return!m.isStripeChosen()||(!(!m.isStripeSaveCardChosen()&&!m.hasSource())||(!!(m.isBancontactChosen()||m.isGiropayChosen()||m.isIdealChosen()||m.isAlipayChosen()||m.isSofortChosen()||m.isP24Chosen()||m.isEpsChosen()||m.isMultibancoChosen())||(m.block(),m.createSource(),!1)))},onCCFormChange:function(){m.reset()},reset:function(){c(".wc-stripe-error, .stripe-source").remove()},onSepaError:function(e){var r=m.getSelectedPaymentElement().parents("li").eq(0).find(".stripe-source-errors");if(!e.error)return c(r).html("");console.log(e.error.message),c(r).html('<ul class="woocommerce_error woocommerce-error wc-stripe-error"><li /></ul>'),c(r).find("li").text(e.error.message)},onError:function(e,r){var t,n=r.error.message,o=m.getSelectedPaymentElement().closest("li"),i=o.find(".woocommerce-SavedPaymentMethods-tokenInput");if(i.length){var s=i.filter(":checked");t=s.closest(".woocommerce-SavedPaymentMethods-new").length?c("#wc-stripe-cc-form .stripe-source-errors"):s.closest("li").find(".stripe-source-errors")}else t=o.find(".stripe-source-errors");if(m.isSepaChosen()&&"invalid_owner_name"===r.error.code&&wc_stripe_params.hasOwnProperty(r.error.code)){var a='<ul class="woocommerce-error"><li /></ul>';return a.find("li").text(wc_stripe_params[r.error.code]),m.submitError(a)}"email_invalid"===r.error.code?n=wc_stripe_params.email_invalid:"invalid_request_error"!==r.error.type&&"api_connection_error"!==r.error.type&&"api_error"!==r.error.type&&"authentication_error"!==r.error.type&&"rate_limit_error"!==r.error.type||(n=wc_stripe_params.invalid_request_error),"card_error"===r.error.type&&wc_stripe_params.hasOwnProperty(r.error.code)&&(n=wc_stripe_params[r.error.code]),"validation_error"===r.error.type&&wc_stripe_params.hasOwnProperty(r.error.code)&&(n=wc_stripe_params[r.error.code]),m.reset(),c(".woocommerce-NoticeGroup-checkout").remove(),console.log(r.error.message),c(t).html('<ul class="woocommerce_error woocommerce-error wc-stripe-error"><li /></ul>'),c(t).find("li").text(n),c(".wc-stripe-error").length&&c("html, body").animate({scrollTop:c(".wc-stripe-error").offset().top-200},200),m.unblock(),c.unblockUI()},submitError:function(e){c(".woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message").remove(),m.form.prepend('<div class="woocommerce-NoticeGroup woocommerce-NoticeGroup-checkout">'+e+"</div>"),m.form.removeClass("processing").unblock(),m.form.find(".input-text, select, input:checkbox").blur();var r="";c("#add_payment_method").length&&(r=c("#add_payment_method")),c("#order_review").length&&(r=c("#order_review")),c("form.checkout").length&&(r=c("form.checkout")),r.length&&c("html, body").animate({scrollTop:r.offset().top-100},500),c(document.body).trigger("checkout_error"),m.unblock()},onHashChange:function(){var e=window.location.hash.match(/^#?confirm-pi-([^:]+):(.+)$/);if(e&&!(e.length<3)){var r=e[1],t=decodeURIComponent(e[2]);window.location.hash="",m.openIntentModal(r,t)}},maybeConfirmIntent:function(){if(c("#stripe-intent-id").length&&c("#stripe-intent-return").length){var e=c("#stripe-intent-id").val(),r=c("#stripe-intent-return").val();m.openIntentModal(e,r,!0)}},openIntentModal:function(e,r,t){n.handleCardPayment(e).then(function(e){if(e.error)throw e.error;"requires_capture"!==e.paymentIntent.status&&"succeeded"!==e.paymentIntent.status||(window.location=r)}).catch(function(e){if(t)return window.location=r;c(document.body).trigger("stripeError",{error:e}),m.form&&m.form.removeClass("processing"),c.get(r+"&is_ajax")})}};m.init()});
1
+ jQuery(function(c){"use strict";try{var o=Stripe(wc_stripe_params.key)}catch(e){return void console.log(e)}var t,n,i,e=Object.keys(wc_stripe_params.elements_options).length?wc_stripe_params.elements_options:{},r=Object.keys(wc_stripe_params.sepa_elements_options).length?wc_stripe_params.sepa_elements_options:{},s=o.elements(e),a=s.create("iban",r),m={getAjaxURL:function(e){return wc_stripe_params.ajaxurl.toString().replace("%%endpoint%%","wc_stripe_"+e)},unmountElements:function(){"yes"===wc_stripe_params.inline_cc_form?t.unmount("#stripe-card-element"):(t.unmount("#stripe-card-element"),n.unmount("#stripe-exp-element"),i.unmount("#stripe-cvc-element"))},mountElements:function(){if(c("#stripe-card-element").length){if("yes"===wc_stripe_params.inline_cc_form)return t.mount("#stripe-card-element");t.mount("#stripe-card-element"),n.mount("#stripe-exp-element"),i.mount("#stripe-cvc-element")}},createElements:function(){var e={base:{iconColor:"#666EE8",color:"#31325F",fontSize:"15px","::placeholder":{color:"#CFD7E0"}}},r={focus:"focused",empty:"empty",invalid:"invalid"};e=wc_stripe_params.elements_styling?wc_stripe_params.elements_styling:e,r=wc_stripe_params.elements_classes?wc_stripe_params.elements_classes:r,"yes"===wc_stripe_params.inline_cc_form?(t=s.create("card",{style:e,hidePostalCode:!0})).addEventListener("change",function(e){m.onCCFormChange(),e.error&&c(document.body).trigger("stripeError",e)}):(t=s.create("cardNumber",{style:e,classes:r}),n=s.create("cardExpiry",{style:e,classes:r}),i=s.create("cardCvc",{style:e,classes:r}),t.addEventListener("change",function(e){m.onCCFormChange(),m.updateCardBrand(e.brand),e.error&&c(document.body).trigger("stripeError",e)}),n.addEventListener("change",function(e){m.onCCFormChange(),e.error&&c(document.body).trigger("stripeError",e)}),i.addEventListener("change",function(e){m.onCCFormChange(),e.error&&c(document.body).trigger("stripeError",e)})),"yes"===wc_stripe_params.is_checkout?c(document.body).on("updated_checkout",function(){t&&m.unmountElements(),m.mountElements(),c("#stripe-iban-element").length&&a.mount("#stripe-iban-element")}):(c("form#add_payment_method").length||c("form#order_review").length)&&(m.mountElements(),c("#stripe-iban-element").length&&a.mount("#stripe-iban-element"))},updateCardBrand:function(e){var r={visa:"stripe-visa-brand",mastercard:"stripe-mastercard-brand",amex:"stripe-amex-brand",discover:"stripe-discover-brand",diners:"stripe-diners-brand",jcb:"stripe-jcb-brand",unknown:"stripe-credit-card-brand"},t=c(".stripe-card-brand"),n="stripe-credit-card-brand";e in r&&(n=r[e]),c.each(r,function(e,r){t.removeClass(r)}),t.addClass(n)},init:function(){"yes"!==wc_stripe_params.is_change_payment_page&&"yes"!==wc_stripe_params.is_pay_for_order_page||c(document.body).trigger("wc-credit-card-form-init"),c("form.woocommerce-checkout").length&&(this.form=c("form.woocommerce-checkout")),c("form.woocommerce-checkout").on("checkout_place_order_stripe checkout_place_order_stripe_bancontact checkout_place_order_stripe_sofort checkout_place_order_stripe_giropay checkout_place_order_stripe_ideal checkout_place_order_stripe_alipay checkout_place_order_stripe_sepa",this.onSubmit),c("form#order_review").length&&(this.form=c("form#order_review")),c("form#order_review, form#add_payment_method").on("submit",this.onSubmit),c("form#add_payment_method").length&&(this.form=c("form#add_payment_method")),c("form.woocommerce-checkout").on("change",this.reset),c(document).on("stripeError",this.onError).on("checkout_error",this.reset),a.on("change",this.onSepaError),m.createElements(),window.addEventListener("hashchange",m.onHashChange),m.maybeConfirmIntent()},isStripeChosen:function(){return c("#payment_method_stripe, #payment_method_stripe_bancontact, #payment_method_stripe_sofort, #payment_method_stripe_giropay, #payment_method_stripe_ideal, #payment_method_stripe_alipay, #payment_method_stripe_sepa, #payment_method_stripe_eps, #payment_method_stripe_multibanco").is(":checked")||c("#payment_method_stripe").is(":checked")&&"new"===c('input[name="wc-stripe-payment-token"]:checked').val()||c("#payment_method_stripe_sepa").is(":checked")&&"new"===c('input[name="wc-stripe-payment-token"]:checked').val()},isStripeSaveCardChosen:function(){return c("#payment_method_stripe").is(":checked")&&c('input[name="wc-stripe-payment-token"]').is(":checked")&&"new"!==c('input[name="wc-stripe-payment-token"]:checked').val()||c("#payment_method_stripe_sepa").is(":checked")&&c('input[name="wc-stripe_sepa-payment-token"]').is(":checked")&&"new"!==c('input[name="wc-stripe_sepa-payment-token"]:checked').val()},isStripeCardChosen:function(){return c("#payment_method_stripe").is(":checked")},isBancontactChosen:function(){return c("#payment_method_stripe_bancontact").is(":checked")},isGiropayChosen:function(){return c("#payment_method_stripe_giropay").is(":checked")},isIdealChosen:function(){return c("#payment_method_stripe_ideal").is(":checked")},isSofortChosen:function(){return c("#payment_method_stripe_sofort").is(":checked")},isAlipayChosen:function(){return c("#payment_method_stripe_alipay").is(":checked")},isSepaChosen:function(){return c("#payment_method_stripe_sepa").is(":checked")},isP24Chosen:function(){return c("#payment_method_stripe_p24").is(":checked")},isEpsChosen:function(){return c("#payment_method_stripe_eps").is(":checked")},isMultibancoChosen:function(){return c("#payment_method_stripe_multibanco").is(":checked")},hasSource:function(){return 0<c("input.stripe-source").length},isMobile:function(){return!!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)},block:function(){m.isMobile()||m.form.block({message:null,overlayCSS:{background:"#fff",opacity:.6}})},unblock:function(){m.form&&m.form.unblock()},getSelectedPaymentElement:function(){return c('.payment_methods input[name="payment_method"]:checked')},getOwnerDetails:function(){var e=c("#billing_first_name").length?c("#billing_first_name").val():wc_stripe_params.billing_first_name,r=c("#billing_last_name").length?c("#billing_last_name").val():wc_stripe_params.billing_last_name,t={name:"",address:{},email:"",phone:""};return t.name=e,t.name=e&&r?e+" "+r:c("#stripe-payment-data").data("full-name"),t.email=c("#billing_email").val(),t.phone=c("#billing_phone").val(),(void 0===t.phone||t.phone.length<=0)&&delete t.phone,(void 0===t.email||t.email.length<=0)&&(c("#stripe-payment-data").data("email").length?t.email=c("#stripe-payment-data").data("email"):delete t.email),(void 0===t.name||t.name.length<=0)&&delete t.name,t.address.line1=c("#billing_address_1").val()||wc_stripe_params.billing_address_1,t.address.line2=c("#billing_address_2").val()||wc_stripe_params.billing_address_2,t.address.state=c("#billing_state").val()||wc_stripe_params.billing_state,t.address.city=c("#billing_city").val()||wc_stripe_params.billing_city,t.address.postal_code=c("#billing_postcode").val()||wc_stripe_params.billing_postcode,t.address.country=c("#billing_country").val()||wc_stripe_params.billing_country,{owner:t}},createSource:function(){var e=m.getOwnerDetails();return m.isSepaChosen()?(e.currency=c("#stripe-sepa_debit-payment-data").data("currency"),e.mandate={notification_method:wc_stripe_params.sepa_mandate_notification},e.type="sepa_debit",o.createSource(a,e).then(m.sourceResponse)):o.createSource(t,e).then(m.sourceResponse)},sourceResponse:function(e){if(e.error)return c(document.body).trigger("stripeError",e);m.reset(),m.form.append(c('<input type="hidden" />').addClass("stripe-source").attr("name","stripe_source").val(e.source.id)),c("form#add_payment_method").length&&c(m.form).off("submit",m.form.onSubmit),m.form.submit()},onSubmit:function(){return!m.isStripeChosen()||(!(!m.isStripeSaveCardChosen()&&!m.hasSource())||(!!(m.isBancontactChosen()||m.isGiropayChosen()||m.isIdealChosen()||m.isAlipayChosen()||m.isSofortChosen()||m.isP24Chosen()||m.isEpsChosen()||m.isMultibancoChosen())||(m.block(),m.createSource(),!1)))},onCCFormChange:function(){m.reset()},reset:function(){c(".wc-stripe-error, .stripe-source").remove()},onSepaError:function(e){var r=m.getSelectedPaymentElement().parents("li").eq(0).find(".stripe-source-errors");if(!e.error)return c(r).html("");console.log(e.error.message),c(r).html('<ul class="woocommerce_error woocommerce-error wc-stripe-error"><li /></ul>'),c(r).find("li").text(e.error.message)},onError:function(e,r){var t,n=r.error.message,o=m.getSelectedPaymentElement().closest("li"),i=o.find(".woocommerce-SavedPaymentMethods-tokenInput");if(i.length){var s=i.filter(":checked");t=s.closest(".woocommerce-SavedPaymentMethods-new").length?c("#wc-stripe-cc-form .stripe-source-errors"):s.closest("li").find(".stripe-source-errors")}else t=o.find(".stripe-source-errors");if(m.isSepaChosen()&&"invalid_owner_name"===r.error.code&&wc_stripe_params.hasOwnProperty(r.error.code)){var a='<ul class="woocommerce-error"><li /></ul>';return a.find("li").text(wc_stripe_params[r.error.code]),m.submitError(a)}"email_invalid"===r.error.code?n=wc_stripe_params.email_invalid:"invalid_request_error"!==r.error.type&&"api_connection_error"!==r.error.type&&"api_error"!==r.error.type&&"authentication_error"!==r.error.type&&"rate_limit_error"!==r.error.type||(n=wc_stripe_params.invalid_request_error),"card_error"===r.error.type&&wc_stripe_params.hasOwnProperty(r.error.code)&&(n=wc_stripe_params[r.error.code]),"validation_error"===r.error.type&&wc_stripe_params.hasOwnProperty(r.error.code)&&(n=wc_stripe_params[r.error.code]),m.reset(),c(".woocommerce-NoticeGroup-checkout").remove(),console.log(r.error.message),c(t).html('<ul class="woocommerce_error woocommerce-error wc-stripe-error"><li /></ul>'),c(t).find("li").text(n),c(".wc-stripe-error").length&&c("html, body").animate({scrollTop:c(".wc-stripe-error").offset().top-200},200),m.unblock(),c.unblockUI()},submitError:function(e){c(".woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message").remove(),m.form.prepend('<div class="woocommerce-NoticeGroup woocommerce-NoticeGroup-checkout">'+e+"</div>"),m.form.removeClass("processing").unblock(),m.form.find(".input-text, select, input:checkbox").blur();var r="";c("#add_payment_method").length&&(r=c("#add_payment_method")),c("#order_review").length&&(r=c("#order_review")),c("form.checkout").length&&(r=c("form.checkout")),r.length&&c("html, body").animate({scrollTop:r.offset().top-100},500),c(document.body).trigger("checkout_error"),m.unblock()},onHashChange:function(){var e=window.location.hash.match(/^#?confirm-(pi|si)-([^:]+):(.+)$/);if(e&&!(e.length<4)){var r=e[1],t=e[2],n=decodeURIComponent(e[3]);window.location.hash="",m.openIntentModal(t,n,!1,"si"===r)}},maybeConfirmIntent:function(){if(c("#stripe-intent-id").length&&c("#stripe-intent-return").length){var e=c("#stripe-intent-id").val(),r=c("#stripe-intent-return").val();m.openIntentModal(e,r,!0,!1)}},openIntentModal:function(e,t,r,n){o[n?"handleCardSetup":"handleCardPayment"](e).then(function(e){if(e.error)throw e.error;var r=e[n?"setupIntent":"paymentIntent"];"requires_capture"!==r.status&&"succeeded"!==r.status||(window.location=t)}).catch(function(e){if(r)return window.location=t;c(document.body).trigger("stripeError",{error:e}),m.form&&m.form.removeClass("processing"),c.get(t+"&is_ajax")})}};m.init()});
changelog.txt CHANGED
@@ -1,5 +1,15 @@
1
  *** Changelog ***
2
 
 
 
 
 
 
 
 
 
 
 
3
  = 4.2.5 - 2019-10-02 =
4
  * Fix - WooCommerce Subscriptions that use only the Stripe customer ID can again be renewed
5
 
1
  *** Changelog ***
2
 
3
+ = 4.3.0 2019-10-17 =
4
+ * Add - For WooCommerce Subscriptions optimize the payment flow for subsequent subscription payments when authentication may be required by using the `setup_future_usage` parameter for the first subscription payment
5
+ * Add - Allow customer to authenticate payment even if they are not charged right away for WooCommerce Subscriptions and Pre-Orders, for example for a WooCommerce Subscription that has a free trial
6
+ * Add - When an off-session payment requires authentication, create a link for customers to come back to the store to authenticate the payment
7
+ * Add - Send an email to WooCommerce Subscription and Pre-Orders customers who need to authenticate a payment that was automatically tried on their behalf
8
+ * Add - When an off-session payment requires authentication, send an email to the admin
9
+ * Add - Admin notice about SCA-readiness
10
+ * Fix - Avoid idempotency key errors for Pre-Orders
11
+ * Fix - Use unique anchor for link about checkout styling changes
12
+
13
  = 4.2.5 - 2019-10-02 =
14
  * Fix - WooCommerce Subscriptions that use only the Stripe customer ID can again be renewed
15
 
includes/abstracts/abstract-wc-stripe-payment-gateway.php CHANGED
@@ -72,21 +72,6 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
72
  );
73
  }
74
 
75
- /**
76
- * Checks to see if error is of invalid request
77
- * error and source is already consumed.
78
- *
79
- * @since 4.1.0
80
- * @param array $error
81
- */
82
- public function is_source_already_consumed_error( $error ) {
83
- return (
84
- $error &&
85
- 'invalid_request_error' === $error->type &&
86
- preg_match( '/The reusable source you provided is consumed because it was previously charged without being attached to a customer or was detached from a customer. To charge a reusable source multiple time you must attach it to a customer first./i', $error->message )
87
- );
88
- }
89
-
90
  /**
91
  * Checks to see if error is of invalid request
92
  * error and it is no such customer.
@@ -548,7 +533,11 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
548
  * @return bool
549
  */
550
  public function is_prepaid_card( $source_object ) {
551
- return ( $source_object && 'token' === $source_object->object && 'prepaid' === $source_object->card->funding );
 
 
 
 
552
  }
553
 
554
  /**
@@ -589,7 +578,6 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
589
  */
590
  public function prepare_source( $user_id, $force_save_source = false ) {
591
  $customer = new WC_Stripe_Customer( $user_id );
592
- $set_customer = true;
593
  $force_save_source = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
594
  $source_object = '';
595
  $source_id = '';
@@ -644,16 +632,15 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
644
  throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
645
  }
646
  } else {
647
- $set_customer = false;
648
  $source_id = $stripe_token;
649
  $is_token = true;
650
  }
651
  }
652
 
653
- if ( ! $set_customer ) {
654
- $customer_id = false;
655
- } else {
656
- $customer_id = $customer->get_id() ? $customer->get_id() : false;
657
  }
658
 
659
  if ( empty( $source_object ) && ! $is_token ) {
@@ -1021,13 +1008,13 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
1021
  }
1022
 
1023
  /**
1024
- * Create a new PaymentIntent.
1025
  *
1026
  * @param WC_Order $order The order that is being paid for.
1027
  * @param object $prepared_source The source that is used for the payment.
1028
- * @return object An intent or an error.
1029
  */
1030
- public function create_intent( $order, $prepared_source ) {
1031
  // The request for a charge contains metadata for the intent.
1032
  $full_request = $this->generate_payment_request( $order, $prepared_source );
1033
 
@@ -1037,7 +1024,6 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
1037
  'currency' => strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() ),
1038
  'description' => $full_request['description'],
1039
  'metadata' => $full_request['metadata'],
1040
- 'statement_descriptor' => $full_request['statement_descriptor'],
1041
  'capture_method' => ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual',
1042
  'payment_method_types' => array(
1043
  'card',
@@ -1048,6 +1034,31 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
1048
  $request['customer'] = $prepared_source->customer;
1049
  }
1050
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
  // Create an intent that awaits an action.
1052
  $intent = WC_Stripe_API::request( $request, 'payment_intents' );
1053
  if ( ! empty( $intent->error ) ) {
@@ -1167,11 +1178,22 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
1167
  $intent_id = $order->get_meta( '_stripe_intent_id' );
1168
  }
1169
 
1170
- if ( ! $intent_id ) {
1171
- return false;
 
 
 
 
 
 
 
 
 
 
 
1172
  }
1173
 
1174
- return WC_Stripe_API::request( array(), "payment_intents/$intent_id", 'GET' );
1175
  }
1176
 
1177
  /**
@@ -1208,4 +1230,106 @@ abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
1208
  $order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1209
  delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1210
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1211
  }
72
  );
73
  }
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  /**
76
  * Checks to see if error is of invalid request
77
  * error and it is no such customer.
533
  * @return bool
534
  */
535
  public function is_prepaid_card( $source_object ) {
536
+ return (
537
+ $source_object
538
+ && ( 'token' === $source_object->object || 'source' === $source_object->object )
539
+ && 'prepaid' === $source_object->card->funding
540
+ );
541
  }
542
 
543
  /**
578
  */
579
  public function prepare_source( $user_id, $force_save_source = false ) {
580
  $customer = new WC_Stripe_Customer( $user_id );
 
581
  $force_save_source = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
582
  $source_object = '';
583
  $source_id = '';
632
  throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
633
  }
634
  } else {
 
635
  $source_id = $stripe_token;
636
  $is_token = true;
637
  }
638
  }
639
 
640
+ $customer_id = $customer->get_id();
641
+ if ( ! $customer_id ) {
642
+ $customer->set_id( $customer->create_customer() );
643
+ $customer_id = $customer->get_id();
644
  }
645
 
646
  if ( empty( $source_object ) && ! $is_token ) {
1008
  }
1009
 
1010
  /**
1011
+ * Generates the request when creating a new payment intent.
1012
  *
1013
  * @param WC_Order $order The order that is being paid for.
1014
  * @param object $prepared_source The source that is used for the payment.
1015
+ * @return array The arguments for the request.
1016
  */
1017
+ public function generate_create_intent_request( $order, $prepared_source ) {
1018
  // The request for a charge contains metadata for the intent.
1019
  $full_request = $this->generate_payment_request( $order, $prepared_source );
1020
 
1024
  'currency' => strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() ),
1025
  'description' => $full_request['description'],
1026
  'metadata' => $full_request['metadata'],
 
1027
  'capture_method' => ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual',
1028
  'payment_method_types' => array(
1029
  'card',
1034
  $request['customer'] = $prepared_source->customer;
1035
  }
1036
 
1037
+ if ( isset( $full_request['statement_descriptor'] ) ) {
1038
+ $request['statement_descriptor'] = $full_request['statement_descriptor'];
1039
+ }
1040
+
1041
+ /**
1042
+ * Filter the return value of the WC_Payment_Gateway_CC::generate_create_intent_request.
1043
+ *
1044
+ * @since 3.1.0
1045
+ * @param array $request
1046
+ * @param WC_Order $order
1047
+ * @param object $source
1048
+ */
1049
+ return apply_filters( 'wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1050
+ }
1051
+
1052
+ /**
1053
+ * Create a new PaymentIntent.
1054
+ *
1055
+ * @param WC_Order $order The order that is being paid for.
1056
+ * @param object $prepared_source The source that is used for the payment.
1057
+ * @return object An intent or an error.
1058
+ */
1059
+ public function create_intent( $order, $prepared_source ) {
1060
+ $request = $this->generate_create_intent_request( $order, $prepared_source );
1061
+
1062
  // Create an intent that awaits an action.
1063
  $intent = WC_Stripe_API::request( $request, 'payment_intents' );
1064
  if ( ! empty( $intent->error ) ) {
1178
  $intent_id = $order->get_meta( '_stripe_intent_id' );
1179
  }
1180
 
1181
+ if ( $intent_id ) {
1182
+ return WC_Stripe_API::request( array(), "payment_intents/$intent_id", 'GET' );
1183
+ }
1184
+
1185
+ // The order doesn't have a payment intent, but it may have a setup intent.
1186
+ if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
1187
+ $intent_id = get_post_meta( $order_id, '_stripe_setup_intent', true );
1188
+ } else {
1189
+ $intent_id = $order->get_meta( '_stripe_setup_intent' );
1190
+ }
1191
+
1192
+ if ( $intent_id ) {
1193
+ return WC_Stripe_API::request( array(), "setup_intents/$intent_id", 'GET' );
1194
  }
1195
 
1196
+ return false;
1197
  }
1198
 
1199
  /**
1230
  $order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1231
  delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1232
  }
1233
+
1234
+ /**
1235
+ * Given a response from Stripe, check if it's a card error where authentication is required
1236
+ * to complete the payment.
1237
+ *
1238
+ * @param object $response The response from Stripe.
1239
+ * @return boolean Whether or not it's a 'authentication_required' error
1240
+ */
1241
+ public function is_authentication_required_for_payment( $response ) {
1242
+ return ( ! empty( $response->error ) && 'authentication_required' === $response->error->code )
1243
+ || ( ! empty( $response->last_payment_error ) && 'authentication_required' === $response->last_payment_error->code );
1244
+ }
1245
+
1246
+ /**
1247
+ * Creates a SetupIntent for future payments, and saves it to the order.
1248
+ *
1249
+ * @param WC_Order $order The ID of the (free/pre- order).
1250
+ * @param object $prepared_source The source, entered/chosen by the customer.
1251
+ * @return string The client secret of the intent, used for confirmation in JS.
1252
+ */
1253
+ public function setup_intent( $order, $prepared_source ) {
1254
+ $order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1255
+ $setup_intent = WC_Stripe_API::request( array(
1256
+ 'payment_method' => $prepared_source->source,
1257
+ 'customer' => $prepared_source->customer,
1258
+ 'confirm' => 'true',
1259
+ ), 'setup_intents' );
1260
+
1261
+ if ( is_wp_error( $setup_intent ) ) {
1262
+ WC_Stripe_Logger::log( "Unable to create SetupIntent for Order #$order_id: " . print_r( $setup_intent, true ) );
1263
+ } elseif ( 'requires_action' === $setup_intent->status ) {
1264
+ if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
1265
+ update_post_meta( $order_id, '_stripe_setup_intent', $setup_intent->id );
1266
+ } else {
1267
+ $order->update_meta_data( '_stripe_setup_intent', $setup_intent->id );
1268
+ $order->save();
1269
+ }
1270
+
1271
+ return $setup_intent->client_secret;
1272
+ }
1273
+ }
1274
+
1275
+ /**
1276
+ * Create and confirm a new PaymentIntent.
1277
+ *
1278
+ * @param WC_Order $order The order that is being paid for.
1279
+ * @param object $prepared_source The source that is used for the payment.
1280
+ * @param float $amount The amount to charge. If not specified, it will be read from the order.
1281
+ * @return object An intent or an error.
1282
+ */
1283
+ public function create_and_confirm_intent_for_off_session( $order, $prepared_source, $amount = NULL ) {
1284
+ // The request for a charge contains metadata for the intent.
1285
+ $full_request = $this->generate_payment_request( $order, $prepared_source );
1286
+
1287
+ $request = array(
1288
+ 'amount' => $amount ? WC_Stripe_Helper::get_stripe_amount( $amount, $full_request['currency'] ) : $full_request['amount'],
1289
+ 'currency' => $full_request['currency'],
1290
+ 'description' => $full_request['description'],
1291
+ 'metadata' => $full_request['metadata'],
1292
+ 'payment_method_types' => array(
1293
+ 'card',
1294
+ ),
1295
+ 'off_session' => 'true',
1296
+ 'confirm' => 'true',
1297
+ 'confirmation_method' => 'automatic',
1298
+ );
1299
+
1300
+ if ( isset( $full_request['statement_descriptor'] ) ) {
1301
+ $request['statement_descriptor'] = $full_request['statement_descriptor'];
1302
+ }
1303
+
1304
+ if ( isset( $full_request['customer'] ) ) {
1305
+ $request['customer'] = $full_request['customer'];
1306
+ }
1307
+
1308
+ if ( isset( $full_request['source'] ) ) {
1309
+ $request['source'] = $full_request['source'];
1310
+ }
1311
+
1312
+ $intent = WC_Stripe_API::request( $request, 'payment_intents' );
1313
+ $is_authentication_required = $this->is_authentication_required_for_payment( $intent );
1314
+
1315
+ if ( ! empty( $intent->error ) && ! $is_authentication_required ) {
1316
+ return $intent;
1317
+ }
1318
+
1319
+ $intent_id = ( ! empty( $intent->error )
1320
+ ? $intent->error->payment_intent->id
1321
+ : $intent->id
1322
+ );
1323
+ $payment_intent = ( ! empty( $intent->error )
1324
+ ? $intent->error->payment_intent
1325
+ : $intent
1326
+ );
1327
+ $order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1328
+ WC_Stripe_Logger::log( "Stripe PaymentIntent $intent_id initiated for order $order_id" );
1329
+
1330
+ // Save the intent ID to the order.
1331
+ $this->save_intent_to_order( $order, $payment_intent );
1332
+
1333
+ return $intent;
1334
+ }
1335
  }
includes/admin/class-wc-stripe-admin-notices.php CHANGED
@@ -23,6 +23,7 @@ class WC_Stripe_Admin_Notices {
23
  public function __construct() {
24
  add_action( 'admin_notices', array( $this, 'admin_notices' ) );
25
  add_action( 'wp_loaded', array( $this, 'hide_notices' ) );
 
26
  }
27
 
28
  /**
@@ -106,6 +107,7 @@ class WC_Stripe_Admin_Notices {
106
  $show_phpver_notice = get_option( 'wc_stripe_show_phpver_notice' );
107
  $show_wcver_notice = get_option( 'wc_stripe_show_wcver_notice' );
108
  $show_curl_notice = get_option( 'wc_stripe_show_curl_notice' );
 
109
  $options = get_option( 'woocommerce_stripe_settings' );
110
  $testmode = ( isset( $options['testmode'] ) && 'yes' === $options['testmode'] ) ? true : false;
111
  $test_pub_key = isset( $options['test_publishable_key'] ) ? $options['test_publishable_key'] : '';
@@ -126,9 +128,9 @@ class WC_Stripe_Admin_Notices {
126
 
127
  if ( empty( $show_style_notice ) ) {
128
  /* translators: 1) int version 2) int version */
129
- $message = __( 'WooCommerce Stripe - We recently made changes to Stripe that may impact the appearance of your checkout. If your checkout has changed unexpectedly, please follow these <a href="https://docs.woocommerce.com/document/stripe/#section-45" target="_blank">instructions</a> to fix.', 'woocommerce-gateway-stripe' );
130
 
131
- $this->add_admin_notice( 'style', 'error', $message, true );
132
 
133
  return;
134
  }
@@ -199,6 +201,10 @@ class WC_Stripe_Admin_Notices {
199
  $this->add_admin_notice( 'ssl', 'notice notice-warning', sprintf( __( 'Stripe is enabled, but a SSL certificate is not detected. Your checkout may not be secure! Please ensure your server has a valid <a href="%1$s" target="_blank">SSL certificate</a>', 'woocommerce-gateway-stripe' ), 'https://en.wikipedia.org/wiki/Transport_Layer_Security' ), true );
200
  }
201
  }
 
 
 
 
202
  }
203
  }
204
 
@@ -292,6 +298,9 @@ class WC_Stripe_Admin_Notices {
292
  case 'SOFORT':
293
  update_option( 'wc_stripe_show_sofort_notice', 'no' );
294
  break;
 
 
 
295
  }
296
  }
297
  }
@@ -310,6 +319,25 @@ class WC_Stripe_Admin_Notices {
310
 
311
  return admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . $section_slug );
312
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  }
314
 
315
  new WC_Stripe_Admin_Notices();
23
  public function __construct() {
24
  add_action( 'admin_notices', array( $this, 'admin_notices' ) );
25
  add_action( 'wp_loaded', array( $this, 'hide_notices' ) );
26
+ add_action( 'woocommerce_stripe_updated', array( $this, 'stripe_updated' ) );
27
  }
28
 
29
  /**
107
  $show_phpver_notice = get_option( 'wc_stripe_show_phpver_notice' );
108
  $show_wcver_notice = get_option( 'wc_stripe_show_wcver_notice' );
109
  $show_curl_notice = get_option( 'wc_stripe_show_curl_notice' );
110
+ $show_sca_notice = get_option( 'wc_stripe_show_sca_notice' );
111
  $options = get_option( 'woocommerce_stripe_settings' );
112
  $testmode = ( isset( $options['testmode'] ) && 'yes' === $options['testmode'] ) ? true : false;
113
  $test_pub_key = isset( $options['test_publishable_key'] ) ? $options['test_publishable_key'] : '';
128
 
129
  if ( empty( $show_style_notice ) ) {
130
  /* translators: 1) int version 2) int version */
131
+ $message = __( 'WooCommerce Stripe - We recently made changes to Stripe that may impact the appearance of your checkout. If your checkout has changed unexpectedly, please follow these <a href="https://docs.woocommerce.com/document/stripe/#styling" target="_blank">instructions</a> to fix.', 'woocommerce-gateway-stripe' );
132
 
133
+ $this->add_admin_notice( 'style', 'notice notice-warning', $message, true );
134
 
135
  return;
136
  }
201
  $this->add_admin_notice( 'ssl', 'notice notice-warning', sprintf( __( 'Stripe is enabled, but a SSL certificate is not detected. Your checkout may not be secure! Please ensure your server has a valid <a href="%1$s" target="_blank">SSL certificate</a>', 'woocommerce-gateway-stripe' ), 'https://en.wikipedia.org/wiki/Transport_Layer_Security' ), true );
202
  }
203
  }
204
+
205
+ if ( empty( $show_sca_notice ) ) {
206
+ $this->add_admin_notice( 'sca', 'notice notice-success', sprintf( __( 'Stripe is now ready for Strong Customer Authentication (SCA) and 3D Secure 2! <a href="%1$s" target="_blank">Read about SCA</a>', 'woocommerce-gateway-stripe' ), 'https://woocommerce.com/posts/introducing-strong-customer-authentication-sca/' ), true );
207
+ }
208
  }
209
  }
210
 
298
  case 'SOFORT':
299
  update_option( 'wc_stripe_show_sofort_notice', 'no' );
300
  break;
301
+ case 'sca':
302
+ update_option( 'wc_stripe_show_sca_notice', 'no' );
303
+ break;
304
  }
305
  }
306
  }
319
 
320
  return admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . $section_slug );
321
  }
322
+
323
+ /**
324
+ * Saves options in order to hide notices based on the gateway's version.
325
+ *
326
+ * @since 4.3.0
327
+ */
328
+ public function stripe_updated() {
329
+ $previous_version = get_option( 'wc_stripe_version' );
330
+
331
+ // Only show the style notice if the plugin was installed and older than 4.1.4.
332
+ if ( empty( $previous_version ) || version_compare( $previous_version, '4.1.4', 'ge' ) ) {
333
+ update_option( 'wc_stripe_show_style_notice', 'no' );
334
+ }
335
+
336
+ // Only show the SCA notice on pre-4.3.0 installs.
337
+ if ( empty( $previous_version ) || version_compare( $previous_version, '4.3.0', 'ge' ) ) {
338
+ update_option( 'wc_stripe_show_sca_notice', 'no' );
339
+ }
340
+ }
341
  }
342
 
343
  new WC_Stripe_Admin_Notices();
includes/class-wc-gateway-stripe.php CHANGED
@@ -139,6 +139,7 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
139
  add_action( 'woocommerce_account_view-order_endpoint', array( $this, 'check_intent_status_on_order_page' ), 1 );
140
  add_filter( 'woocommerce_payment_successful_result', array( $this, 'modify_successful_payment_result' ), 99999, 2 );
141
  add_action( 'set_logged_in_cookie', array( $this, 'set_cookie_on_current_request' ) );
 
142
 
143
  if ( WC_Stripe_Helper::is_pre_orders_exists() ) {
144
  $this->pre_orders = new WC_Stripe_Pre_Orders_Compat();
@@ -248,29 +249,17 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
248
  }
249
 
250
  if ( is_add_payment_method_page() ) {
251
- $pay_button_text = __( 'Add Card', 'woocommerce-gateway-stripe' );
252
- $total = '';
253
  $firstname = $user->user_firstname;
254
  $lastname = $user->user_lastname;
255
-
256
- } elseif ( function_exists( 'wcs_order_contains_subscription' ) && isset( $_GET['change_payment_method'] ) ) { // wpcs: csrf ok.
257
- $pay_button_text = __( 'Change Payment Method', 'woocommerce-gateway-stripe' );
258
- $total = '';
259
- } else {
260
- $pay_button_text = '';
261
  }
262
 
263
  ob_start();
264
 
265
  echo '<div
266
  id="stripe-payment-data"
267
- data-panel-label="' . esc_attr( $pay_button_text ) . '"
268
  data-email="' . esc_attr( $user_email ) . '"
269
- data-amount="' . esc_attr( WC_Stripe_Helper::get_stripe_amount( $total ) ) . '"
270
- data-name="' . esc_attr( $this->statement_descriptor ) . '"
271
  data-full-name="' . esc_attr( $firstname . ' ' . $lastname ) . '"
272
  data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '"
273
- data-allow-remember-me="' . esc_attr( apply_filters( 'wc_stripe_allow_remember_me', true ) ? 'true' : 'false' ) . '"
274
  >';
275
 
276
  if ( $this->testmode ) {
@@ -383,7 +372,7 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
383
  * @version 4.0.0
384
  */
385
  public function payment_scripts() {
386
- if ( ! is_product() && ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() && ! isset( $_GET['change_payment_method'] ) ) { // wpcs: csrf ok.
387
  return;
388
  }
389
 
@@ -476,23 +465,6 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
476
  wp_enqueue_script( 'woocommerce_stripe' );
477
  }
478
 
479
- /**
480
- * Creates a new WC_Stripe_Customer if the visitor chooses to.
481
- *
482
- * @since 4.2.0
483
- * @param WC_Order $order The order that is being created.
484
- */
485
- public function maybe_create_customer( $order ) {
486
- // This comes from the create account checkbox in the checkout page.
487
- if ( empty( $_POST['createaccount'] ) ) { // wpcs: csrf ok.
488
- return;
489
- }
490
-
491
- $new_customer_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->customer_user : $order->get_customer_id();
492
- $new_stripe_customer = new WC_Stripe_Customer( $new_customer_id );
493
- $new_stripe_customer->create_customer();
494
- }
495
-
496
  /**
497
  * Checks if a source object represents a prepaid credit card and
498
  * throws an exception if it is one, but that is not allowed.
@@ -554,20 +526,33 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
554
  * Completes an order without a positive value.
555
  *
556
  * @since 4.2.0
557
- * @param WC_Order $order The order to complete.
558
- * @return array Redirection data for `process_payment`.
 
 
559
  */
560
- public function complete_free_order( $order ) {
561
- $order->payment_complete();
 
 
 
 
 
 
 
 
 
 
 
 
562
 
563
  // Remove cart.
564
  WC()->cart->empty_cart();
565
 
 
 
566
  // Return thank you page redirect.
567
- return array(
568
- 'result' => 'success',
569
- 'redirect' => $this->get_return_url( $order ),
570
- );
571
  }
572
 
573
  /**
@@ -593,8 +578,6 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
593
  return $this->pre_orders->process_pre_order( $order_id );
594
  }
595
 
596
- $this->maybe_create_customer( $order );
597
-
598
  $prepared_source = $this->prepare_source( get_current_user_id(), $force_save_source );
599
 
600
  $this->maybe_disallow_prepaid_card( $prepared_source );
@@ -602,7 +585,7 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
602
  $this->save_source_to_order( $order, $prepared_source );
603
 
604
  if ( 0 >= $order->get_total() ) {
605
- return $this->complete_free_order( $order );
606
  }
607
 
608
  // This will throw exception if not valid.
@@ -611,6 +594,10 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
611
  WC_Stripe_Logger::log( "Info: Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
612
 
613
  $intent = $this->get_intent_from_order( $order );
 
 
 
 
614
  if ( $intent ) {
615
  $intent = $this->update_existing_intent( $intent, $order, $prepared_source );
616
  } else {
@@ -658,9 +645,9 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
658
  */
659
 
660
  return array(
661
- 'result' => 'success',
662
- 'redirect' => $this->get_return_url( $order ),
663
- 'intent_secret' => $intent->client_secret,
664
  );
665
  }
666
  }
@@ -670,7 +657,9 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
670
  $this->process_response( $response, $order );
671
 
672
  // Remove cart.
673
- WC()->cart->empty_cart();
 
 
674
 
675
  // Unlock the order.
676
  $this->unlock_order_payment( $order );
@@ -836,9 +825,16 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
836
  return $gateways;
837
  }
838
 
 
 
 
 
 
 
 
839
  add_filter( 'woocommerce_checkout_show_terms', '__return_false' );
840
  add_filter( 'woocommerce_pay_order_button_html', '__return_false' );
841
- add_filter( 'woocommerce_available_payment_gateways', array( $this, '__return_empty_array' ) );
842
  add_filter( 'woocommerce_no_available_payment_methods_message', array( $this, 'change_no_available_methods_message' ) );
843
  add_action( 'woocommerce_pay_order_after_submit', array( $this, 'render_payment_intent_inputs' ) );
844
 
@@ -856,14 +852,52 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
856
  return wpautop( __( "Almost there!\n\nYour order has already been created, the only thing that still needs to be done is for you to authorize the payment with your bank.", 'woocommerce-gateway-stripe' ) );
857
  }
858
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
  /**
860
  * Renders hidden inputs on the "Pay for Order" page in order to let Stripe handle PaymentIntents.
861
  *
 
 
 
862
  * @since 4.2
863
  */
864
- public function render_payment_intent_inputs() {
865
- $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
866
- $intent = $this->get_intent_from_order( $order );
 
 
 
 
867
 
868
  $verification_url = add_query_arg(
869
  array(
@@ -875,7 +909,7 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
875
  WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
876
  );
877
 
878
- echo '<input type="hidden" id="stripe-intent-id" value="' . esc_attr( $intent->client_secret ) . '" />';
879
  echo '<input type="hidden" id="stripe-intent-return" value="' . esc_attr( $verification_url ) . '" />';
880
  }
881
 
@@ -917,7 +951,7 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
917
  /**
918
  * Attached to `woocommerce_payment_successful_result` with a late priority,
919
  * this method will combine the "naturally" generated redirect URL from
920
- * WooCommerce and a payment intent secret into a hash, which contains both
921
  * the secret, and a proper URL, which will confirm whether the intent succeeded.
922
  *
923
  * @since 4.2.0
@@ -926,8 +960,8 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
926
  * @return array
927
  */
928
  public function modify_successful_payment_result( $result, $order_id ) {
929
- // Only redirects with intents need to be modified.
930
- if ( ! isset( $result['intent_secret'] ) ) {
931
  return $result;
932
  }
933
 
@@ -941,8 +975,11 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
941
  WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
942
  );
943
 
944
- // Combine into a hash.
945
- $redirect = sprintf( '#confirm-pi-%s:%s', $result['intent_secret'], rawurlencode( $verification_url ) );
 
 
 
946
 
947
  return array(
948
  'result' => 'success',
@@ -990,7 +1027,14 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
990
  return;
991
  }
992
 
993
- if ( 'succeeded' === $intent->status || 'requires_capture' === $intent->status ) {
 
 
 
 
 
 
 
994
  // Proceed with the payment completion.
995
  $this->process_response( end( $intent->charges->data ), $order );
996
  } else if ( 'requires_payment_method' === $intent->status ) {
@@ -1015,12 +1059,26 @@ class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
1015
  }
1016
 
1017
  // Load the right message and update the status.
1018
- $status_message = ( $intent->last_payment_error )
1019
  /* translators: 1) The error message that was received from Stripe. */
1020
  ? sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $intent->last_payment_error->message )
1021
  : __( 'Stripe SCA authentication failed.', 'woocommerce-gateway-stripe' );
1022
  $order->update_status( 'failed', $status_message );
 
1023
 
1024
- $this->send_failed_order_email( $order->get_id() );
 
 
 
 
 
 
 
 
 
 
 
 
 
1025
  }
1026
  }
139
  add_action( 'woocommerce_account_view-order_endpoint', array( $this, 'check_intent_status_on_order_page' ), 1 );
140
  add_filter( 'woocommerce_payment_successful_result', array( $this, 'modify_successful_payment_result' ), 99999, 2 );
141
  add_action( 'set_logged_in_cookie', array( $this, 'set_cookie_on_current_request' ) );
142
+ add_filter( 'woocommerce_get_checkout_payment_url', array( $this, 'get_checkout_payment_url' ), 10, 2 );
143
 
144
  if ( WC_Stripe_Helper::is_pre_orders_exists() ) {
145
  $this->pre_orders = new WC_Stripe_Pre_Orders_Compat();
249
  }
250
 
251
  if ( is_add_payment_method_page() ) {
 
 
252
  $firstname = $user->user_firstname;
253
  $lastname = $user->user_lastname;
 
 
 
 
 
 
254
  }
255
 
256
  ob_start();
257
 
258
  echo '<div
259
  id="stripe-payment-data"
 
260
  data-email="' . esc_attr( $user_email ) . '"
 
 
261
  data-full-name="' . esc_attr( $firstname . ' ' . $lastname ) . '"
262
  data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '"
 
263
  >';
264
 
265
  if ( $this->testmode ) {
372
  * @version 4.0.0
373
  */
374
  public function payment_scripts() {
375
+ if ( ! is_product() && ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() && ! isset( $_GET['change_payment_method'] ) || ( is_order_received_page() ) ) { // wpcs: csrf ok.
376
  return;
377
  }
378
 
465
  wp_enqueue_script( 'woocommerce_stripe' );
466
  }
467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  /**
469
  * Checks if a source object represents a prepaid credit card and
470
  * throws an exception if it is one, but that is not allowed.
526
  * Completes an order without a positive value.
527
  *
528
  * @since 4.2.0
529
+ * @param WC_Order $order The order to complete.
530
+ * @param WC_Order $prepared_source Payment source and customer data.
531
+ * @param boolean $force_save_source Whether the payment source must be saved, like when dealing with a Subscription setup.
532
+ * @return array Redirection data for `process_payment`.
533
  */
534
+ public function complete_free_order( $order, $prepared_source, $force_save_source ) {
535
+ $response = array(
536
+ 'result' => 'success',
537
+ 'redirect' => $this->get_return_url( $order ),
538
+ );
539
+
540
+ if ( $force_save_source ) {
541
+ $intent_secret = $this->setup_intent( $order, $prepared_source );
542
+
543
+ if ( ! empty( $intent_secret ) ) {
544
+ $response['setup_intent_secret'] = $intent_secret;
545
+ return $response;
546
+ }
547
+ }
548
 
549
  // Remove cart.
550
  WC()->cart->empty_cart();
551
 
552
+ $order->payment_complete();
553
+
554
  // Return thank you page redirect.
555
+ return $response;
 
 
 
556
  }
557
 
558
  /**
578
  return $this->pre_orders->process_pre_order( $order_id );
579
  }
580
 
 
 
581
  $prepared_source = $this->prepare_source( get_current_user_id(), $force_save_source );
582
 
583
  $this->maybe_disallow_prepaid_card( $prepared_source );
585
  $this->save_source_to_order( $order, $prepared_source );
586
 
587
  if ( 0 >= $order->get_total() ) {
588
+ return $this->complete_free_order( $order, $prepared_source, $force_save_source );
589
  }
590
 
591
  // This will throw exception if not valid.
594
  WC_Stripe_Logger::log( "Info: Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
595
 
596
  $intent = $this->get_intent_from_order( $order );
597
+ if ( isset( $intent->object ) && 'setup_intent' === $intent->object ) {
598
+ $intent = false; // This function can only deal with *payment* intents
599
+ }
600
+
601
  if ( $intent ) {
602
  $intent = $this->update_existing_intent( $intent, $order, $prepared_source );
603
  } else {
645
  */
646
 
647
  return array(
648
+ 'result' => 'success',
649
+ 'redirect' => $this->get_return_url( $order ),
650
+ 'payment_intent_secret' => $intent->client_secret,
651
  );
652
  }
653
  }
657
  $this->process_response( $response, $order );
658
 
659
  // Remove cart.
660
+ if ( isset( WC()->cart ) ) {
661
+ WC()->cart->empty_cart();
662
+ }
663
 
664
  // Unlock the order.
665
  $this->unlock_order_payment( $order );
825
  return $gateways;
826
  }
827
 
828
+ try {
829
+ $this->prepare_intent_for_order_pay_page();
830
+ } catch ( WC_Stripe_Exception $e ) {
831
+ // Just show the full order pay page if there was a problem preparing the Payment Intent
832
+ return $gateways;
833
+ }
834
+
835
  add_filter( 'woocommerce_checkout_show_terms', '__return_false' );
836
  add_filter( 'woocommerce_pay_order_button_html', '__return_false' );
837
+ add_filter( 'woocommerce_available_payment_gateways', '__return_empty_array' );
838
  add_filter( 'woocommerce_no_available_payment_methods_message', array( $this, 'change_no_available_methods_message' ) );
839
  add_action( 'woocommerce_pay_order_after_submit', array( $this, 'render_payment_intent_inputs' ) );
840
 
852
  return wpautop( __( "Almost there!\n\nYour order has already been created, the only thing that still needs to be done is for you to authorize the payment with your bank.", 'woocommerce-gateway-stripe' ) );
853
  }
854
 
855
+ /**
856
+ * Prepares the Payment Intent for it to be completed in the "Pay for Order" page.
857
+ *
858
+ * @param WC_Order|null $order Order object, or null to get the order from the "order-pay" URL parameter
859
+ *
860
+ * @throws WC_Stripe_Exception
861
+ * @since 4.3
862
+ */
863
+ public function prepare_intent_for_order_pay_page( $order = null ) {
864
+ if ( ! isset( $order ) || empty( $order ) ) {
865
+ $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
866
+ }
867
+ $intent = $this->get_intent_from_order( $order );
868
+
869
+ if ( ! $intent ) {
870
+ throw new WC_Stripe_Exception( 'Payment Intent not found', __( 'Payment Intent not found for order #' . $order->get_id(), 'woocommerce-gateway-stripe' ) );
871
+ }
872
+
873
+ if ( 'requires_payment_method' === $intent->status && isset( $intent->last_payment_error )
874
+ && 'authentication_required' === $intent->last_payment_error->code ) {
875
+ $intent = WC_Stripe_API::request( array(
876
+ 'payment_method' => $intent->last_payment_error->source->id,
877
+ ), 'payment_intents/' . $intent->id . '/confirm' );
878
+ if ( isset( $intent->error ) ) {
879
+ throw new WC_Stripe_Exception( print_r( $intent, true ), $intent->error->message );
880
+ }
881
+ }
882
+
883
+ $this->order_pay_intent = $intent;
884
+ }
885
+
886
  /**
887
  * Renders hidden inputs on the "Pay for Order" page in order to let Stripe handle PaymentIntents.
888
  *
889
+ * @param WC_Order|null $order Order object, or null to get the order from the "order-pay" URL parameter
890
+ *
891
+ * @throws WC_Stripe_Exception
892
  * @since 4.2
893
  */
894
+ public function render_payment_intent_inputs( $order = null ) {
895
+ if ( ! isset( $order ) || empty( $order ) ) {
896
+ $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
897
+ }
898
+ if ( ! isset( $this->order_pay_intent ) ) {
899
+ $this->prepare_intent_for_order_pay_page( $order );
900
+ }
901
 
902
  $verification_url = add_query_arg(
903
  array(
909
  WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
910
  );
911
 
912
+ echo '<input type="hidden" id="stripe-intent-id" value="' . esc_attr( $this->order_pay_intent->client_secret ) . '" />';
913
  echo '<input type="hidden" id="stripe-intent-return" value="' . esc_attr( $verification_url ) . '" />';
914
  }
915
 
951
  /**
952
  * Attached to `woocommerce_payment_successful_result` with a late priority,
953
  * this method will combine the "naturally" generated redirect URL from
954
+ * WooCommerce and a payment/setup intent secret into a hash, which contains both
955
  * the secret, and a proper URL, which will confirm whether the intent succeeded.
956
  *
957
  * @since 4.2.0
960
  * @return array
961
  */
962
  public function modify_successful_payment_result( $result, $order_id ) {
963
+ if ( ! isset( $result['payment_intent_secret'] ) && ! isset( $result['setup_intent_secret'] ) ) {
964
+ // Only redirects with intents need to be modified.
965
  return $result;
966
  }
967
 
975
  WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
976
  );
977
 
978
+ if ( isset( $result['payment_intent_secret'] ) ) {
979
+ $redirect = sprintf( '#confirm-pi-%s:%s', $result['payment_intent_secret'], rawurlencode( $verification_url ) );
980
+ } else if ( isset( $result['setup_intent_secret'] ) ) {
981
+ $redirect = sprintf( '#confirm-si-%s:%s', $result['setup_intent_secret'], rawurlencode( $verification_url ) );
982
+ }
983
 
984
  return array(
985
  'result' => 'success',
1027
  return;
1028
  }
1029
 
1030
+ if ( 'setup_intent' === $intent->object && 'succeeded' === $intent->status ) {
1031
+ WC()->cart->empty_cart();
1032
+ if ( WC_Stripe_Helper::is_pre_orders_exists() && WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
1033
+ WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
1034
+ } else {
1035
+ $order->payment_complete();
1036
+ }
1037
+ } else if ( 'succeeded' === $intent->status || 'requires_capture' === $intent->status ) {
1038
  // Proceed with the payment completion.
1039
  $this->process_response( end( $intent->charges->data ), $order );
1040
  } else if ( 'requires_payment_method' === $intent->status ) {
1059
  }
1060
 
1061
  // Load the right message and update the status.
1062
+ $status_message = isset( $intent->last_payment_error )
1063
  /* translators: 1) The error message that was received from Stripe. */
1064
  ? sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $intent->last_payment_error->message )
1065
  : __( 'Stripe SCA authentication failed.', 'woocommerce-gateway-stripe' );
1066
  $order->update_status( 'failed', $status_message );
1067
+ }
1068
 
1069
+ /**
1070
+ * Preserves the "wc-stripe-confirmation" URL parameter so the user can complete the SCA authentication after logging in.
1071
+ *
1072
+ * @param string $pay_url Current computed checkout URL for the given order.
1073
+ * @param WC_Order $order Order object.
1074
+ *
1075
+ * @return string Checkout URL for the given order.
1076
+ */
1077
+ public function get_checkout_payment_url( $pay_url, $order ) {
1078
+ global $wp;
1079
+ if ( isset( $_GET['wc-stripe-confirmation'] ) && isset( $wp->query_vars['order-pay'] ) && $wp->query_vars['order-pay'] == $order->get_id() ) {
1080
+ $pay_url = add_query_arg( 'wc-stripe-confirmation', 1, $pay_url );
1081
+ }
1082
+ return $pay_url;
1083
  }
1084
  }
includes/class-wc-stripe-api.php CHANGED
@@ -14,7 +14,7 @@ class WC_Stripe_API {
14
  * Stripe API Endpoint
15
  */
16
  const ENDPOINT = 'https://api.stripe.com/v1/';
17
- const STRIPE_API_VERSION = '2019-02-19';
18
 
19
  /**
20
  * Secret API Key.
14
  * Stripe API Endpoint
15
  */
16
  const ENDPOINT = 'https://api.stripe.com/v1/';
17
+ const STRIPE_API_VERSION = '2019-09-09';
18
 
19
  /**
20
  * Secret API Key.
includes/class-wc-stripe-customer.php CHANGED
@@ -116,16 +116,23 @@ class WC_Stripe_Customer {
116
  $billing_last_name = get_user_meta( $user->ID, 'last_name', true );
117
  }
118
 
119
- $description = __( 'Name', 'woocommerce-gateway-stripe' ) . ': ' . $billing_first_name . ' ' . $billing_last_name . ' ' . __( 'Username', 'woocommerce-gateway-stripe' ) . ': ' . $user->user_login;
 
120
 
121
  $defaults = array(
122
  'email' => $user->user_email,
123
  'description' => $description,
124
  );
125
  } else {
 
 
 
 
 
 
126
  $defaults = array(
127
  'email' => $billing_email,
128
- 'description' => '',
129
  );
130
  }
131
 
@@ -171,10 +178,9 @@ class WC_Stripe_Customer {
171
  /**
172
  * Add a source for this stripe customer.
173
  * @param string $source_id
174
- * @param bool $retry
175
  * @return WP_Error|int
176
  */
177
- public function add_source( $source_id, $retry = true ) {
178
  if ( ! $this->get_id() ) {
179
  $this->set_id( $this->create_customer() );
180
  }
@@ -195,7 +201,7 @@ class WC_Stripe_Customer {
195
  if ( $this->is_no_such_customer_error( $response->error ) ) {
196
  delete_user_meta( $this->get_user_id(), '_stripe_customer_id' );
197
  $this->create_customer();
198
- return $this->add_source( $source_id, false );
199
  } else {
200
  return $response;
201
  }
116
  $billing_last_name = get_user_meta( $user->ID, 'last_name', true );
117
  }
118
 
119
+ // translators: %1$s First name, %2$s Second name, %3$s Username.
120
+ $description = sprintf( __( 'Name: %1$s %2$s, Username: %s', 'woocommerce-gateway-stripe' ), $billing_first_name, $billing_last_name, $user->user_login );
121
 
122
  $defaults = array(
123
  'email' => $user->user_email,
124
  'description' => $description,
125
  );
126
  } else {
127
+ $billing_first_name = isset( $_POST['billing_first_name'] ) ? filter_var( wp_unslash( $_POST['billing_first_name'] ), FILTER_SANITIZE_STRING ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
128
+ $billing_last_name = isset( $_POST['billing_last_name'] ) ? filter_var( wp_unslash( $_POST['billing_last_name'] ), FILTER_SANITIZE_STRING ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
129
+
130
+ // translators: %1$s First name, %2$s Second name.
131
+ $description = sprintf( __( 'Name: %1$s %2$s, Guest', 'woocommerce-gateway-stripe' ), $billing_first_name, $billing_last_name );
132
+
133
  $defaults = array(
134
  'email' => $billing_email,
135
+ 'description' => $description,
136
  );
137
  }
138
 
178
  /**
179
  * Add a source for this stripe customer.
180
  * @param string $source_id
 
181
  * @return WP_Error|int
182
  */
183
+ public function add_source( $source_id ) {
184
  if ( ! $this->get_id() ) {
185
  $this->set_id( $this->create_customer() );
186
  }
201
  if ( $this->is_no_such_customer_error( $response->error ) ) {
202
  delete_user_meta( $this->get_user_id(), '_stripe_customer_id' );
203
  $this->create_customer();
204
+ return $this->add_source( $source_id );
205
  } else {
206
  return $response;
207
  }
includes/class-wc-stripe-helper.php CHANGED
@@ -453,6 +453,25 @@ class WC_Stripe_Helper {
453
  return false;
454
  }
455
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  /**
457
  * Sanitize statement descriptor text.
458
  *
453
  return false;
454
  }
455
 
456
+ /**
457
+ * Gets the order by Stripe SetupIntent ID.
458
+ *
459
+ * @since 4.3
460
+ * @param string $intent_id The ID of the intent.
461
+ * @return WC_Order|bool Either an order or false when not found.
462
+ */
463
+ public static function get_order_by_setup_intent_id( $intent_id ) {
464
+ global $wpdb;
465
+
466
+ $order_id = $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT ID FROM $wpdb->posts as posts LEFT JOIN $wpdb->postmeta as meta ON posts.ID = meta.post_id WHERE meta.meta_value = %s AND meta.meta_key = %s", $intent_id, '_stripe_setup_intent' ) );
467
+
468
+ if ( ! empty( $order_id ) ) {
469
+ return wc_get_order( $order_id );
470
+ }
471
+
472
+ return false;
473
+ }
474
+
475
  /**
476
  * Sanitize statement descriptor text.
477
  *
includes/class-wc-stripe-intent-controller.php CHANGED
@@ -92,7 +92,7 @@ class WC_Stripe_Intent_Controller {
92
  wc_add_notice( esc_html( $message ), 'error' );
93
 
94
  $redirect_url = $woocommerce->cart->is_empty()
95
- ? get_permalink( woocommerce_get_page_id( 'shop' ) )
96
  : wc_get_checkout_url();
97
 
98
  $this->handle_error( $e, $redirect_url );
92
  wc_add_notice( esc_html( $message ), 'error' );
93
 
94
  $redirect_url = $woocommerce->cart->is_empty()
95
+ ? get_permalink( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? woocommerce_get_page_id( 'shop' ) : wc_get_page_id( 'shop' ) )
96
  : wc_get_checkout_url();
97
 
98
  $this->handle_error( $e, $redirect_url );
includes/class-wc-stripe-webhook-handler.php CHANGED
@@ -81,7 +81,6 @@ class WC_Stripe_Webhook_Handler extends WC_Stripe_Payment_Gateway {
81
  *
82
  * @since 4.0.0
83
  * @version 4.0.0
84
- * @todo Implement proper webhook signature validation. Ref https://stripe.com/docs/webhooks#signatures
85
  * @param string $request_headers The request headers from Stripe.
86
  * @param string $request_body The request body from Stripe.
87
  * @return bool
@@ -693,6 +692,43 @@ class WC_Stripe_Webhook_Handler extends WC_Stripe_Payment_Gateway {
693
  $this->unlock_order_payment( $order );
694
  }
695
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  /**
697
  * Processes the incoming webhook.
698
  *
@@ -744,6 +780,11 @@ class WC_Stripe_Webhook_Handler extends WC_Stripe_Payment_Gateway {
744
  case 'payment_intent.payment_failed':
745
  case 'payment_intent.amount_capturable_updated':
746
  $this->process_payment_intent_success( $notification );
 
 
 
 
 
747
 
748
  }
749
  }
81
  *
82
  * @since 4.0.0
83
  * @version 4.0.0
 
84
  * @param string $request_headers The request headers from Stripe.
85
  * @param string $request_body The request body from Stripe.
86
  * @return bool
692
  $this->unlock_order_payment( $order );
693
  }
694
 
695
+ public function process_setup_intent( $notification ) {
696
+ $intent = $notification->data->object;
697
+ $order = WC_Stripe_Helper::get_order_by_setup_intent_id( $intent->id );
698
+
699
+ if ( ! $order ) {
700
+ WC_Stripe_Logger::log( 'Could not find order via setup intent ID: ' . $intent->id );
701
+ return;
702
+ }
703
+
704
+ if ( 'pending' !== $order->get_status() && 'failed' !== $order->get_status() ) {
705
+ return;
706
+ }
707
+
708
+ if ( $this->lock_order_payment( $order, $intent ) ) {
709
+ return;
710
+ }
711
+
712
+ $order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
713
+ if ( 'setup_intent.succeeded' === $notification->type ) {
714
+ WC_Stripe_Logger::log( "Stripe SetupIntent $intent->id succeeded for order $order_id" );
715
+ if ( WC_Stripe_Helper::is_pre_orders_exists() && WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
716
+ WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
717
+ } else {
718
+ $order->payment_complete();
719
+ }
720
+ } else {
721
+ $error_message = $intent->last_setup_error ? $intent->last_setup_error->message : "";
722
+
723
+ /* translators: 1) The error message that was received from Stripe. */
724
+ $order->update_status( 'failed', sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message ) );
725
+
726
+ $this->send_failed_order_email( $order_id );
727
+ }
728
+
729
+ $this->unlock_order_payment( $order );
730
+ }
731
+
732
  /**
733
  * Processes the incoming webhook.
734
  *
780
  case 'payment_intent.payment_failed':
781
  case 'payment_intent.amount_capturable_updated':
782
  $this->process_payment_intent_success( $notification );
783
+ break;
784
+
785
+ case 'setup_intent.succeeded':
786
+ case 'setup_intent.setup_failed':
787
+ $this->process_setup_intent( $notification );
788
 
789
  }
790
  }
includes/compat/class-wc-stripe-email-failed-authentication-retry.php ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Admin email about payment retry failed due to authentication
4
+ *
5
+ * Email sent to admins when an attempt to automatically process a subscription renewal payment has failed
6
+ * with the `authentication_needed` error, and a retry rule has been applied to retry the payment in the future.
7
+ *
8
+ * @version 4.3.0
9
+ * @package WooCommerce_Stripe/Classes/WC_Stripe_Email_Failed_Authentication_Retry
10
+ * @extends WC_Email_Failed_Order
11
+ */
12
+
13
+ if ( ! defined( 'ABSPATH' ) ) {
14
+ exit;
15
+ }
16
+
17
+ /**
18
+ * An email sent to the admin when payment fails to go through due to authentication_required error.
19
+ *
20
+ * @since 4.3.0
21
+ */
22
+ class WC_Stripe_Email_Failed_Authentication_Retry extends WC_Email_Failed_Order {
23
+
24
+ /**
25
+ * Constructor
26
+ */
27
+ public function __construct() {
28
+ $this->id = 'failed_authentication_requested';
29
+ $this->title = __( 'Payment Authentication Requested Email', 'woocommerce-gateway-stripe' );
30
+ $this->description = __( 'Payment authentication requested emails are sent to chosen recipient(s) when an attempt to automatically process a subscription renewal payment fails because the transaction requires an SCA verification, the customer is requested to authenticate the payment, and a retry rule has been applied to notify the customer again within a certain time period.', 'woocommerce-gateway-stripe' );
31
+
32
+ $this->heading = __( 'Automatic renewal payment failed due to authentication required', 'woocommerce-gateway-stripe' );
33
+ $this->subject = __( '[{site_title}] Automatic payment failed for {order_number}. Customer asked to authenticate payment and will be notified again {retry_time}', 'woocommerce-gateway-stripe' );
34
+
35
+ $this->template_html = 'emails/failed-renewal-authentication-requested.php';
36
+ $this->template_plain = 'emails/plain/failed-renewal-authentication-requested.php';
37
+ $this->template_base = plugin_dir_path( WC_STRIPE_MAIN_FILE ) . 'templates/';
38
+
39
+ $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
40
+
41
+ // We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor.
42
+ WC_Email::__construct();
43
+ }
44
+
45
+ /**
46
+ * Get the default e-mail subject.
47
+ *
48
+ * @return string
49
+ */
50
+ public function get_default_subject() {
51
+ return $this->subject;
52
+ }
53
+
54
+ /**
55
+ * Get the default e-mail heading.
56
+ *
57
+ * @return string
58
+ */
59
+ public function get_default_heading() {
60
+ return $this->heading;
61
+ }
62
+
63
+ /**
64
+ * Trigger.
65
+ *
66
+ * @param int $order_id The order ID.
67
+ * @param WC_Order|null $order Order object.
68
+ */
69
+ public function trigger( $order_id, $order = null ) {
70
+ $this->object = $order;
71
+
72
+ $this->find['retry-time'] = '{retry_time}';
73
+ if ( class_exists( 'WCS_Retry_Manager' ) && function_exists( 'wcs_get_human_time_diff' ) ) {
74
+ $this->retry = WCS_Retry_Manager::store()->get_last_retry_for_order( wcs_get_objects_property( $order, 'id' ) );
75
+ $this->replace['retry-time'] = wcs_get_human_time_diff( $this->retry->get_time() );
76
+ } else {
77
+ WC_Stripe_Logger::log( 'WCS_Retry_Manager class or does not exist. Not able to send admnin email about customer notification for authentication required for renewal payment.' );
78
+ return;
79
+ }
80
+
81
+ $this->find['order-number'] = '{order_number}';
82
+ $this->replace['order-number'] = $this->object->get_order_number();
83
+
84
+ if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
85
+ return;
86
+ }
87
+
88
+ $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
89
+ }
90
+
91
+ /**
92
+ * Get content html.
93
+ *
94
+ * @return string
95
+ */
96
+ public function get_content_html() {
97
+ return wc_get_template_html(
98
+ $this->template_html,
99
+ array(
100
+ 'order' => $this->object,
101
+ 'retry' => $this->retry,
102
+ 'email_heading' => $this->get_heading(),
103
+ 'sent_to_admin' => true,
104
+ 'plain_text' => false,
105
+ 'email' => $this,
106
+ ),
107
+ '',
108
+ $this->template_base
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Get content plain.
114
+ *
115
+ * @return string
116
+ */
117
+ public function get_content_plain() {
118
+ return wc_get_template_html(
119
+ $this->template_plain,
120
+ array(
121
+ 'order' => $this->object,
122
+ 'retry' => $this->retry,
123
+ 'email_heading' => $this->get_heading(),
124
+ 'sent_to_admin' => true,
125
+ 'plain_text' => true,
126
+ 'email' => $this,
127
+ ),
128
+ '',
129
+ $this->template_base
130
+ );
131
+ }
132
+ }
includes/compat/class-wc-stripe-email-failed-authentication.php ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly.
4
+ }
5
+
6
+ /**
7
+ * Base for Failed Renewal/Pre-Order Authentication Notifications.
8
+ *
9
+ * @extends WC_Email
10
+ */
11
+ abstract class WC_Stripe_Email_Failed_Authentication extends WC_Email {
12
+ /**
13
+ * An instance of the email, which would normally be sent after a failed payment.
14
+ *
15
+ * @var WC_Email
16
+ */
17
+ public $original_email;
18
+
19
+ /**
20
+ * Generates the HTML for the email while keeping the `template_base` in mind.
21
+ *
22
+ * @return string
23
+ */
24
+ public function get_content_html() {
25
+ ob_start();
26
+ wc_get_template(
27
+ $this->template_html,
28
+ array(
29
+ 'order' => $this->object,
30
+ 'email_heading' => $this->get_heading(),
31
+ 'sent_to_admin' => false,
32
+ 'plain_text' => false,
33
+ 'authorization_url' => $this->get_authorization_url( $this->object ),
34
+ 'email' => $this,
35
+ ),
36
+ '',
37
+ $this->template_base
38
+ );
39
+ return ob_get_clean();
40
+ }
41
+
42
+ /**
43
+ * Generates the plain text for the email while keeping the `template_base` in mind.
44
+ *
45
+ * @return string
46
+ */
47
+ public function get_content_plain() {
48
+ ob_start();
49
+ wc_get_template(
50
+ $this->template_plain,
51
+ array(
52
+ 'order' => $this->object,
53
+ 'email_heading' => $this->get_heading(),
54
+ 'sent_to_admin' => false,
55
+ 'plain_text' => true,
56
+ 'authorization_url' => $this->get_authorization_url( $this->object ),
57
+ 'email' => $this,
58
+ ),
59
+ '',
60
+ $this->template_base
61
+ );
62
+ return ob_get_clean();
63
+ }
64
+
65
+ /**
66
+ * Generates the URL, which will be used to authenticate the payment.
67
+ *
68
+ * @param WC_Order $order The order whose payment needs authentication.
69
+ * @return string
70
+ */
71
+ public function get_authorization_url( $order ) {
72
+ return add_query_arg( 'wc-stripe-confirmation', 1, $order->get_checkout_payment_url( false ) );
73
+ }
74
+
75
+ /**
76
+ * Uses specific fields from `WC_Email_Customer_Invoice` for this email.
77
+ */
78
+ public function init_form_fields() {
79
+ parent::init_form_fields();
80
+ $base_fields = $this->form_fields;
81
+
82
+ $this->form_fields = array(
83
+ 'enabled' => array(
84
+ 'title' => _x( 'Enable/Disable', 'an email notification', 'woocommerce-gateway-stripe' ),
85
+ 'type' => 'checkbox',
86
+ 'label' => __( 'Enable this email notification', 'woocommerce-gateway-stripe' ),
87
+ 'default' => 'yes',
88
+ ),
89
+
90
+ 'subject' => $base_fields['subject'],
91
+ 'heading' => $base_fields['heading'],
92
+ 'email_type' => $base_fields['email_type'],
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Triggers the email.
98
+ *
99
+ * @param WC_Order $order The renewal order whose payment failed.
100
+ */
101
+ public function trigger( $order ) {
102
+ if ( ! $this->is_enabled() ) {
103
+ return;
104
+ }
105
+
106
+ $this->object = $order;
107
+
108
+ if ( method_exists( $order, 'get_billing_email' ) ) {
109
+ $this->recipient = $order->get_billing_email();
110
+ } else {
111
+ $this->recipient = $order->billing_email;
112
+ }
113
+
114
+ $this->find['order_date'] = '{order_date}';
115
+ if ( function_exists( 'wc_format_datetime' ) ) { // WC 3.0+
116
+ $this->replace['order_date'] = wc_format_datetime( $order->get_date_created() );
117
+ } else { // WC < 3.0
118
+ $this->replace['order_date'] = $order->date_created->date_i18n( wc_date_format() );
119
+ }
120
+
121
+ $this->find['order_number'] = '{order_number}';
122
+ $this->replace['order_number'] = $order->get_order_number();
123
+
124
+ $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
125
+ }
126
+ }
includes/compat/class-wc-stripe-email-failed-preorder-authentication.php ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly.
4
+ }
5
+
6
+ /**
7
+ * Failed Renewal/Pre-Order Authentication Notification
8
+ *
9
+ * @extends WC_Stripe_Email_Failed_Authentication
10
+ */
11
+ class WC_Stripe_Email_Failed_Preorder_Authentication extends WC_Stripe_Email_Failed_Authentication {
12
+ /**
13
+ * Holds the message, which is entered by admins when sending the email.
14
+ *
15
+ * @var string
16
+ */
17
+ protected $custom_message;
18
+
19
+ /**
20
+ * Constructor.
21
+ *
22
+ * @param WC_Email[] $email_classes All existing instances of WooCommerce emails.
23
+ */
24
+ public function __construct( $email_classes = array() ) {
25
+ $this->id = 'failed_preorder_sca_authentication';
26
+ $this->title = __( 'Pre-order Payment Action Needed', 'woocommerce-gateway-stripe' );
27
+ $this->description = __( 'This is an order notification sent to the customer once a pre-order is complete, but additional payment steps are required.', 'woocommerce-gateway-stripe' );
28
+ $this->customer_email = true;
29
+
30
+ $this->template_html = 'emails/failed-preorder-authentication.php';
31
+ $this->template_plain = 'emails/plain/failed-preorder-authentication.php';
32
+ $this->template_base = plugin_dir_path( WC_STRIPE_MAIN_FILE ) . 'templates/';
33
+
34
+ // Use the "authentication required" hook to add the correct, later hook.
35
+ add_action( 'wc_gateway_stripe_process_payment_authentication_required', array( $this, 'trigger' ) );
36
+
37
+ if ( isset( $email_classes['WC_Pre_Orders_Email_Pre_Order_Available'] ) ) {
38
+ $this->original_email = $email_classes['WC_Pre_Orders_Email_Pre_Order_Available'];
39
+ }
40
+
41
+ // We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor.
42
+ parent::__construct();
43
+ }
44
+
45
+ /**
46
+ * When autnentication is required, this adds another action to `wc_pre_orders_pre_order_completed`
47
+ * in order to send the authentication required email when the custom pre-orders message is available.
48
+ *
49
+ * @param WC_Order $order The order whose payment is failing.
50
+ */
51
+ public function trigger( $order ) {
52
+ if ( class_exists( 'WC_Pre_Orders_Order' ) && WC_Pre_Orders_Order::order_contains_pre_order( $order->get_id() ) ) {
53
+ if ( isset( $this->original_email ) ) {
54
+ remove_action( 'wc_pre_order_status_completed_notification', array( $this->original_email, 'trigger' ), 10, 2 );
55
+ }
56
+
57
+ add_action( 'wc_pre_orders_pre_order_completed', array( $this, 'send_email' ), 10, 2 );
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Triggers the email while also disconnecting the original Pre-Orders email.
63
+ *
64
+ * @param WC_Order $order The order that is being paid.
65
+ * @param string $message The message, which should be added to the email.
66
+ */
67
+ public function send_email( $order, $message ) {
68
+ $this->custom_message = $message;
69
+
70
+ parent::trigger( $order );
71
+
72
+ // Restore the action of the original email for other bulk actions.
73
+ if ( isset( $this->original_email ) ) {
74
+ add_action( 'wc_pre_order_status_completed_notification', array( $this->original_email, 'trigger' ), 10, 2 );
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Returns the default subject of the email (modifyable in settings).
80
+ *
81
+ * @return string
82
+ */
83
+ public function get_default_subject() {
84
+ return __( 'Payment authorization needed for pre-order {order_number}', 'woocommerce-gateway-stripe' );
85
+ }
86
+
87
+ /**
88
+ * Returns the default heading of the email (modifyable in settings).
89
+ *
90
+ * @return string
91
+ */
92
+ public function get_default_heading() {
93
+ return __( 'Payment authorization needed for pre-order {order_number}', 'woocommerce-gateway-stripe' );
94
+ }
95
+
96
+ /**
97
+ * Returns the custom message, entered by the admin.
98
+ *
99
+ * @return string
100
+ */
101
+ public function get_custom_message() {
102
+ return $this->custom_message;
103
+ }
104
+ }
includes/compat/class-wc-stripe-email-failed-renewal-authentication.php ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly.
4
+ }
5
+
6
+ /**
7
+ * Failed Renewal/Pre-Order Authentication Notification
8
+ *
9
+ * @extends WC_Email_Customer_Invoice
10
+ */
11
+ class WC_Stripe_Email_Failed_Renewal_Authentication extends WC_Stripe_Email_Failed_Authentication {
12
+ /**
13
+ * Constructor.
14
+ *
15
+ * @param WC_Email[] $email_classes All existing instances of WooCommerce emails.
16
+ */
17
+ public function __construct( $email_classes = array() ) {
18
+ $this->id = 'failed_renewal_authentication';
19
+ $this->title = __( 'Failed Subscription Renewal SCA Authentication', 'woocommerce-gateway-stripe' );
20
+ $this->description = __( 'Sent to a customer when a renewal fails because the transaction requires an SCA verification. The email contains renewal order information and payment links.', 'woocommerce-gateway-stripe' );
21
+ $this->customer_email = true;
22
+
23
+ $this->template_html = 'emails/failed-renewal-authentication.php';
24
+ $this->template_plain = 'emails/plain/failed-renewal-authentication.php';
25
+ $this->template_base = plugin_dir_path( WC_STRIPE_MAIN_FILE ) . 'templates/';
26
+
27
+ // Triggers the email at the correct hook.
28
+ add_action( 'wc_gateway_stripe_process_payment_authentication_required', array( $this, 'trigger' ) );
29
+
30
+ if ( isset( $email_classes['WCS_Email_Customer_Renewal_Invoice'] ) ) {
31
+ $this->original_email = $email_classes['WCS_Email_Customer_Renewal_Invoice'];
32
+ }
33
+
34
+ // We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor.
35
+ parent::__construct();
36
+ }
37
+
38
+ /**
39
+ * Triggers the email while also disconnecting the original Subscriptions email.
40
+ *
41
+ * @param WC_Order $order The order that is being paid.
42
+ */
43
+ public function trigger( $order ) {
44
+ if ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order->get_id() ) || wcs_is_subscription( $order->get_id() ) || wcs_order_contains_renewal( $order->get_id() ) ) ) {
45
+ parent::trigger( $order );
46
+
47
+ // Prevent the renewal email from WooCommerce Subscriptions from being sent.
48
+ if ( isset( $this->original_email ) ) {
49
+ remove_action( 'woocommerce_generated_manual_renewal_order_renewal_notification', array( $this->original_email, 'trigger' ) );
50
+ remove_action( 'woocommerce_order_status_failed_renewal_notification', array( $this->original_email, 'trigger' ) );
51
+ }
52
+
53
+ // Prevent the retry email from WooCommerce Subscriptions from being sent.
54
+ add_filter( 'wcs_get_retry_rule_raw', array( $this, 'prevent_retry_notification_email' ), 100, 3 );
55
+
56
+ // Send email to store owner indicating communication is happening with the customer to request authentication.
57
+ add_filter( 'wcs_get_retry_rule_raw', array( $this, 'set_store_owner_custom_email' ), 100, 3 );
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Returns the default subject of the email (modifyable in settings).
63
+ *
64
+ * @return string
65
+ */
66
+ public function get_default_subject() {
67
+ return __( 'Payment authorization needed for renewal of {site_title} order {order_number}', 'woocommerce-gateway-stripe' );
68
+ }
69
+
70
+ /**
71
+ * Returns the default heading of the email (modifyable in settings).
72
+ *
73
+ * @return string
74
+ */
75
+ public function get_default_heading() {
76
+ return __( 'Payment authorization needed for renewal of order {order_number}', 'woocommerce-gateway-stripe' );
77
+ }
78
+
79
+ /**
80
+ * Prevent all customer-facing retry notifications from being sent after this email.
81
+ *
82
+ * @param array $rule_array The raw details about the retry rule.
83
+ * @param int $retry_number The number of the retry.
84
+ * @param int $order_id The ID of the order that needs payment.
85
+ * @return array
86
+ */
87
+ public function prevent_retry_notification_email( $rule_array, $retry_number, $order_id ) {
88
+ if ( wcs_get_objects_property( $this->object, 'id' ) === $order_id ) {
89
+ $rule_array['email_template_customer'] = '';
90
+ }
91
+
92
+ return $rule_array;
93
+ }
94
+
95
+ /**
96
+ * Send store owner a different email when the retry is related to an authentication required error.
97
+ *
98
+ * @param array $rule_array The raw details about the retry rule.
99
+ * @param int $retry_number The number of the retry.
100
+ * @param int $order_id The ID of the order that needs payment.
101
+ * @return array
102
+ */
103
+ public function set_store_owner_custom_email( $rule_array, $retry_number, $order_id ) {
104
+ if (
105
+ wcs_get_objects_property( $this->object, 'id' ) === $order_id &&
106
+ '' !== $rule_array['email_template_admin'] // Only send our email if a retry admin email was already going to be sent.
107
+ ) {
108
+ $rule_array['email_template_admin'] = 'WC_Stripe_Email_Failed_Authentication_Retry';
109
+ }
110
+
111
+ return $rule_array;
112
+ }
113
+ }
includes/compat/class-wc-stripe-pre-orders-compat.php CHANGED
@@ -28,19 +28,15 @@ class WC_Stripe_Pre_Orders_Compat extends WC_Stripe_Payment_Gateway {
28
  * @param object $order
29
  */
30
  public function remove_order_source_before_retry( $order ) {
31
- $order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
32
- delete_post_meta( $order_id, '_stripe_source_id' );
33
- // For BW compat will remove in the future.
34
- delete_post_meta( $order_id, '_stripe_card_id' );
35
- }
36
-
37
- /**
38
- * Remove order meta
39
- * @param object $order
40
- */
41
- public function remove_order_customer_before_retry( $order ) {
42
- $order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
43
- delete_post_meta( $order_id, '_stripe_customer_id' );
44
  }
45
 
46
  /**
@@ -61,19 +57,29 @@ class WC_Stripe_Pre_Orders_Compat extends WC_Stripe_Payment_Gateway {
61
  throw new WC_Stripe_Exception( __( 'Unable to store payment details. Please try again.', 'woocommerce-gateway-stripe' ) );
62
  }
63
 
 
 
 
 
 
 
64
  $this->save_source_to_order( $order, $prepared_source );
65
 
66
- // Remove cart
 
 
 
 
 
 
 
67
  WC()->cart->empty_cart();
68
 
69
  // Is pre ordered!
70
  WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
71
 
72
  // Return thank you page redirect
73
- return array(
74
- 'result' => 'success',
75
- 'redirect' => $this->get_return_url( $order ),
76
- );
77
  } catch ( WC_Stripe_Exception $e ) {
78
  wc_add_notice( $e->getLocalizedMessage(), 'error' );
79
  WC_Stripe_Logger::log( 'Pre Orders Error: ' . $e->getMessage() );
@@ -87,37 +93,49 @@ class WC_Stripe_Pre_Orders_Compat extends WC_Stripe_Payment_Gateway {
87
 
88