From 7d1d4b72f48ed52fab594bedf171e3a5fda85812 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Mon, 2 Sep 2024 19:20:00 +0600 Subject: [PATCH 001/120] add ece feature flag --- includes/class-wc-stripe-feature-flags.php | 11 +++++++++++ .../class-wc-stripe-upe-payment-gateway.php | 3 +++ 2 files changed, 14 insertions(+) diff --git a/includes/class-wc-stripe-feature-flags.php b/includes/class-wc-stripe-feature-flags.php index e066a5c39..8215962c5 100644 --- a/includes/class-wc-stripe-feature-flags.php +++ b/includes/class-wc-stripe-feature-flags.php @@ -5,6 +5,17 @@ class WC_Stripe_Feature_Flags { const UPE_CHECKOUT_FEATURE_ATTRIBUTE_NAME = 'upe_checkout_experience_enabled'; + const ECE_FEATURE_FLAG_NAME = '_wcstripe_feature_ece'; + + /** + * Checks whether Stripe ECE (Express Checkout Element) feature flag is enabled. + * Express checkout buttons are rendered with either ECE or PRB depending on this feature flag. + * + * @return bool + */ + public static function is_stripe_ece_enabled() { + return 'yes' === get_option( self::ECE_FEATURE_FLAG_NAME, 'no' ); + } /** * Checks whether UPE "preview" feature flag is enabled. diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 46e0d4f0e..8ded73830 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -407,6 +407,9 @@ public function javascript_params() { $stripe_params['blocksAppearance'] = get_transient( $this->get_appearance_transient_key( true ) ); $stripe_params['saveAppearanceNonce'] = wp_create_nonce( 'wc_stripe_save_appearance_nonce' ); + // ECE feature flag + $stripe_params['isECEEnabled'] = WC_Stripe_Feature_Flags::is_stripe_ece_enabled(); + $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); $currency = get_woocommerce_currency(); From a5cc1f0380f91cd0f5b6dd3d47efb8b67592ca9e Mon Sep 17 00:00:00 2001 From: Mayisha Date: Wed, 4 Sep 2024 00:28:09 +0600 Subject: [PATCH 002/120] update '@stripe/react-stripe-js' to latest --- package-lock.json | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e6a37e1e..20f162434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,11 +6,11 @@ "packages": { "": { "name": "woocommerce-gateway-stripe", - "version": "8.5.2", + "version": "8.6.1", "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { - "@stripe/react-stripe-js": "1.4.1", + "@stripe/react-stripe-js": "^2.8.0", "@stripe/stripe-js": "^1.36.0", "@testing-library/react-hooks": "^7.0.2", "framer-motion": "^7.6.1", @@ -4573,16 +4573,16 @@ } }, "node_modules/@stripe/react-stripe-js": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.4.1.tgz", - "integrity": "sha512-FjcVrhf72+9fUL3Lz3xi02ni9tzH1A1x6elXlr6tvBDgSD55oPJuodoP8eC7xTnBIKq0olF5uJvgtkJyDCdzjA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.8.0.tgz", + "integrity": "sha512-Vf1gNEuBxA9EtxiLghm2ZWmgbADNMJw4HW6eolUu0DON/6mZvWZgk0KHolN0sozNJwYp0i/8hBsDBcBUWcvnbw==", "dependencies": { "prop-types": "^15.7.2" }, "peerDependencies": { - "@stripe/stripe-js": "^1.13.0", - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" + "@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@stripe/stripe-js": { @@ -39055,9 +39055,9 @@ } }, "@stripe/react-stripe-js": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.4.1.tgz", - "integrity": "sha512-FjcVrhf72+9fUL3Lz3xi02ni9tzH1A1x6elXlr6tvBDgSD55oPJuodoP8eC7xTnBIKq0olF5uJvgtkJyDCdzjA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.8.0.tgz", + "integrity": "sha512-Vf1gNEuBxA9EtxiLghm2ZWmgbADNMJw4HW6eolUu0DON/6mZvWZgk0KHolN0sozNJwYp0i/8hBsDBcBUWcvnbw==", "requires": { "prop-types": "^15.7.2" } diff --git a/package.json b/package.json index 964ca1a85..d135663aa 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "yarnhook": "^0.5.1" }, "dependencies": { - "@stripe/react-stripe-js": "1.4.1", + "@stripe/react-stripe-js": "^2.8.0", "@stripe/stripe-js": "^1.36.0", "@testing-library/react-hooks": "^7.0.2", "framer-motion": "^7.6.1", From 26c019664aed7ff035d0d44a182a37e3a59ccd1a Mon Sep 17 00:00:00 2001 From: Mayisha Date: Wed, 4 Sep 2024 00:50:22 +0600 Subject: [PATCH 003/120] render ece button on block checkout --- .../express-checkout/apple-pay-preview.js | 6 ++++ client/blocks/express-checkout/constants.js | 2 ++ .../express-checkout/express-checkout.js | 28 +++++++++++++++++++ client/blocks/express-checkout/index.js | 20 +++++++++++++ client/blocks/upe/index.js | 4 +++ 5 files changed, 60 insertions(+) create mode 100644 client/blocks/express-checkout/apple-pay-preview.js create mode 100644 client/blocks/express-checkout/constants.js create mode 100644 client/blocks/express-checkout/express-checkout.js create mode 100644 client/blocks/express-checkout/index.js diff --git a/client/blocks/express-checkout/apple-pay-preview.js b/client/blocks/express-checkout/apple-pay-preview.js new file mode 100644 index 000000000..932b3fa36 --- /dev/null +++ b/client/blocks/express-checkout/apple-pay-preview.js @@ -0,0 +1,6 @@ +const applePayImage = + "data:image/svg+xml,%3Csvg width='264' height='48' viewBox='0 0 264 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='264' height='48' rx='3' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.114 16.6407C125.682 15.93 126.067 14.9756 125.966 14C125.135 14.0415 124.121 14.549 123.533 15.2602C123.006 15.8693 122.539 16.8641 122.661 17.7983C123.594 17.8797 124.526 17.3317 125.114 16.6407Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.955 17.982C124.601 17.9011 123.448 18.7518 122.801 18.7518C122.154 18.7518 121.163 18.0224 120.092 18.0421C118.696 18.0629 117.402 18.8524 116.694 20.1079C115.238 22.6196 116.31 26.3453 117.726 28.3909C118.414 29.4028 119.242 30.5174 120.334 30.4769C121.366 30.4365 121.77 29.8087 123.024 29.8087C124.277 29.8087 124.641 30.4769 125.733 30.4567C126.865 30.4365 127.573 29.4443 128.261 28.4313C129.049 27.2779 129.373 26.1639 129.393 26.1027C129.373 26.0825 127.209 25.2515 127.189 22.7606C127.169 20.6751 128.888 19.6834 128.969 19.6217C127.998 18.1847 126.481 18.0224 125.955 17.982Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M136.131 23.1804H138.834C140.886 23.1804 142.053 22.0752 142.053 20.1592C142.053 18.2432 140.886 17.1478 138.845 17.1478H136.131V23.1804ZM139.466 15.1582C142.411 15.1582 144.461 17.1903 144.461 20.1483C144.461 23.1172 142.369 25.1596 139.392 25.1596H136.131V30.3498H133.775V15.1582H139.466Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M152.198 26.224V25.3712L149.579 25.5397C148.106 25.6341 147.339 26.182 147.339 27.14C147.339 28.0664 148.138 28.6667 149.39 28.6667C150.988 28.6667 152.198 27.6449 152.198 26.224ZM145.046 27.2032C145.046 25.2551 146.529 24.1395 149.263 23.971L152.198 23.7922V22.9498C152.198 21.7181 151.388 21.0442 149.947 21.0442C148.758 21.0442 147.896 21.6548 147.717 22.5916H145.592C145.656 20.6232 147.507 19.1914 150.01 19.1914C152.703 19.1914 154.459 20.602 154.459 22.7917V30.351H152.282V28.5298H152.229C151.609 29.719 150.241 30.4666 148.758 30.4666C146.571 30.4666 145.046 29.1612 145.046 27.2032Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M156.461 34.4145V32.5934C156.608 32.6141 156.965 32.6354 157.155 32.6354C158.196 32.6354 158.785 32.1932 159.142 31.0564L159.353 30.3824L155.366 19.3281H157.827L160.604 28.298H160.657L163.434 19.3281H165.832L161.698 30.9402C160.752 33.6038 159.668 34.4778 157.376 34.4778C157.197 34.4778 156.618 34.4565 156.461 34.4145Z' fill='white'/%3E%3C/svg%3E%0A"; + +const ApplePayPreview = () => ; + +export default ApplePayPreview; diff --git a/client/blocks/express-checkout/constants.js b/client/blocks/express-checkout/constants.js new file mode 100644 index 000000000..be70165e2 --- /dev/null +++ b/client/blocks/express-checkout/constants.js @@ -0,0 +1,2 @@ +export const PAYMENT_METHOD_EXPRESS_CHECKOUT_ELEMENT = + 'express_checkout_element'; diff --git a/client/blocks/express-checkout/express-checkout.js b/client/blocks/express-checkout/express-checkout.js new file mode 100644 index 000000000..e40c5ec07 --- /dev/null +++ b/client/blocks/express-checkout/express-checkout.js @@ -0,0 +1,28 @@ +/* global wc_stripe_payment_request_params */ + +import React from 'react'; +import { Elements, ExpressCheckoutElement } from '@stripe/react-stripe-js'; + +export const ExpressCheckout = ( { stripe } ) => { + const options = { + mode: 'payment', + amount: 1099, + currency: 'usd', + }; + + const buttonOptions = { + buttonType: { + googlePay: wc_stripe_payment_request_params.button.type, + applePay: wc_stripe_payment_request_params.button.type, + }, + }; + + return ( + + console.log( 'confirmed' ) } + /> + + ); +}; diff --git a/client/blocks/express-checkout/index.js b/client/blocks/express-checkout/index.js new file mode 100644 index 000000000..f558b9240 --- /dev/null +++ b/client/blocks/express-checkout/index.js @@ -0,0 +1,20 @@ +import { PAYMENT_METHOD_EXPRESS_CHECKOUT_ELEMENT } from './constants'; +import { ExpressCheckout } from './express-checkout'; +import ApplePayPreview from './apple-pay-preview'; +import { loadStripe } from 'wcstripe/blocks/load-stripe'; +import { getBlocksConfiguration } from 'wcstripe/blocks/utils'; + +const stripePromise = loadStripe(); + +const expressCheckoutElementsPaymentMethod = { + name: PAYMENT_METHOD_EXPRESS_CHECKOUT_ELEMENT, + content: , + edit: , + canMakePayment: () => true, + paymentMethodId: PAYMENT_METHOD_EXPRESS_CHECKOUT_ELEMENT, + supports: { + features: getBlocksConfiguration()?.supports ?? [], + }, +}; + +export default expressCheckoutElementsPaymentMethod; diff --git a/client/blocks/upe/index.js b/client/blocks/upe/index.js index 9a88532d9..a81deb9b5 100644 --- a/client/blocks/upe/index.js +++ b/client/blocks/upe/index.js @@ -8,6 +8,7 @@ import { getDeferredIntentCreationUPEFields } from './upe-deferred-intent-creati import { SavedTokenHandler } from './saved-token-handler'; import { updateTokenLabelsWhenLoaded } from './token-label-updater.js'; import paymentRequestPaymentMethod from 'wcstripe/blocks/payment-request'; +import expressCheckoutElementsPaymentMethod from 'wcstripe/blocks/express-checkout'; import WCStripeAPI from 'wcstripe/api'; import { getBlocksConfiguration } from 'wcstripe/blocks/utils'; import './styles.scss'; @@ -90,5 +91,8 @@ Object.entries( getBlocksConfiguration()?.paymentMethodsConfig ) // Register Stripe Payment Request. registerExpressPaymentMethod( paymentRequestPaymentMethod ); +// Register Express Checkout Element. +registerExpressPaymentMethod( expressCheckoutElementsPaymentMethod ); + // Update token labels when the checkout form is loaded. updateTokenLabelsWhenLoaded(); From fe572d5287a694952de3badba76bad27bd1bf012 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Thu, 5 Sep 2024 01:41:15 +0600 Subject: [PATCH 004/120] display ece button if feature flag is enabled --- client/blocks/upe/index.js | 13 ++++++++----- .../class-wc-stripe-payment-request.php | 1 + .../class-wc-stripe-upe-payment-gateway.php | 3 --- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/client/blocks/upe/index.js b/client/blocks/upe/index.js index a81deb9b5..aeb02578f 100644 --- a/client/blocks/upe/index.js +++ b/client/blocks/upe/index.js @@ -1,3 +1,4 @@ +/* global wc_stripe_payment_request_params */ import { registerPaymentMethod, registerExpressPaymentMethod, @@ -88,11 +89,13 @@ Object.entries( getBlocksConfiguration()?.paymentMethodsConfig ) } ); } ); -// Register Stripe Payment Request. -registerExpressPaymentMethod( paymentRequestPaymentMethod ); - -// Register Express Checkout Element. -registerExpressPaymentMethod( expressCheckoutElementsPaymentMethod ); +if ( wc_stripe_payment_request_params.stripe.is_ece_enabled ) { + // Register Express Checkout Element. + registerExpressPaymentMethod( expressCheckoutElementsPaymentMethod ); +} else { + // Register Stripe Payment Request. + registerExpressPaymentMethod( paymentRequestPaymentMethod ); +} // Update token labels when the checkout form is loaded. updateTokenLabelsWhenLoaded(); diff --git a/includes/payment-methods/class-wc-stripe-payment-request.php b/includes/payment-methods/class-wc-stripe-payment-request.php index 669167fb7..5e12055e8 100644 --- a/includes/payment-methods/class-wc-stripe-payment-request.php +++ b/includes/payment-methods/class-wc-stripe-payment-request.php @@ -778,6 +778,7 @@ public function javascript_params() { 'locale' => WC_Stripe_Helper::convert_wc_locale_to_stripe_locale( get_locale() ), 'is_link_enabled' => WC_Stripe_UPE_Payment_Method_Link::is_link_enabled(), 'is_payment_request_enabled' => $this->is_payment_request_enabled(), + 'is_ece_enabled' => WC_Stripe_Feature_Flags::is_stripe_ece_enabled(), // ECE feature flag. ], 'nonce' => [ 'payment' => wp_create_nonce( 'wc-stripe-payment-request' ), diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 8ded73830..46e0d4f0e 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -407,9 +407,6 @@ public function javascript_params() { $stripe_params['blocksAppearance'] = get_transient( $this->get_appearance_transient_key( true ) ); $stripe_params['saveAppearanceNonce'] = wp_create_nonce( 'wc_stripe_save_appearance_nonce' ); - // ECE feature flag - $stripe_params['isECEEnabled'] = WC_Stripe_Feature_Flags::is_stripe_ece_enabled(); - $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); $currency = get_woocommerce_currency(); From 122db025ef7bf910bab4e27b855f40109d6e95dc Mon Sep 17 00:00:00 2001 From: Mayisha Date: Tue, 10 Sep 2024 00:44:48 +0600 Subject: [PATCH 005/120] check ece feature flag status from blocks data --- client/blocks/upe/index.js | 3 +-- includes/payment-methods/class-wc-stripe-payment-request.php | 1 - .../payment-methods/class-wc-stripe-upe-payment-gateway.php | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/blocks/upe/index.js b/client/blocks/upe/index.js index aeb02578f..3a042e344 100644 --- a/client/blocks/upe/index.js +++ b/client/blocks/upe/index.js @@ -1,4 +1,3 @@ -/* global wc_stripe_payment_request_params */ import { registerPaymentMethod, registerExpressPaymentMethod, @@ -89,7 +88,7 @@ Object.entries( getBlocksConfiguration()?.paymentMethodsConfig ) } ); } ); -if ( wc_stripe_payment_request_params.stripe.is_ece_enabled ) { +if ( getBlocksConfiguration()?.isECEEnabled ) { // Register Express Checkout Element. registerExpressPaymentMethod( expressCheckoutElementsPaymentMethod ); } else { diff --git a/includes/payment-methods/class-wc-stripe-payment-request.php b/includes/payment-methods/class-wc-stripe-payment-request.php index 5e12055e8..669167fb7 100644 --- a/includes/payment-methods/class-wc-stripe-payment-request.php +++ b/includes/payment-methods/class-wc-stripe-payment-request.php @@ -778,7 +778,6 @@ public function javascript_params() { 'locale' => WC_Stripe_Helper::convert_wc_locale_to_stripe_locale( get_locale() ), 'is_link_enabled' => WC_Stripe_UPE_Payment_Method_Link::is_link_enabled(), 'is_payment_request_enabled' => $this->is_payment_request_enabled(), - 'is_ece_enabled' => WC_Stripe_Feature_Flags::is_stripe_ece_enabled(), // ECE feature flag. ], 'nonce' => [ 'payment' => wp_create_nonce( 'wc-stripe-payment-request' ), diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index f0bda040f..5c50cfd5d 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -408,6 +408,9 @@ public function javascript_params() { $stripe_params['blocksAppearance'] = get_transient( $this->get_appearance_transient_key( true ) ); $stripe_params['saveAppearanceNonce'] = wp_create_nonce( 'wc_stripe_save_appearance_nonce' ); + // ECE feature flag + $stripe_params['isECEEnabled'] = WC_Stripe_Feature_Flags::is_stripe_ece_enabled(); + $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); $currency = get_woocommerce_currency(); From 6a8e7ae03a18ca4eddbd81f239ecfa829f5aa6e0 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Tue, 10 Sep 2024 01:14:50 +0600 Subject: [PATCH 006/120] add min height to express checkout container --- .../blocks/express-checkout/express-checkout.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client/blocks/express-checkout/express-checkout.js b/client/blocks/express-checkout/express-checkout.js index e40c5ec07..326c94d91 100644 --- a/client/blocks/express-checkout/express-checkout.js +++ b/client/blocks/express-checkout/express-checkout.js @@ -3,7 +3,8 @@ import React from 'react'; import { Elements, ExpressCheckoutElement } from '@stripe/react-stripe-js'; -export const ExpressCheckout = ( { stripe } ) => { +export const ExpressCheckout = ( props ) => { + const { stripe } = props; const options = { mode: 'payment', amount: 1099, @@ -18,11 +19,13 @@ export const ExpressCheckout = ( { stripe } ) => { }; return ( - - console.log( 'confirmed' ) } - /> - +
+ + console.log( 'clicked' ) } + /> + +
); }; From 34dc409ff07b2a9758761e66b6fd264941c4b86d Mon Sep 17 00:00:00 2001 From: Mayisha Date: Tue, 10 Sep 2024 17:10:28 +0600 Subject: [PATCH 007/120] create 'WC_Stripe_Express_Checkout_Element' class --- ...ass-wc-stripe-express-checkout-element.php | 1745 +++++++++++++++++ woocommerce-gateway-stripe.php | 19 +- 2 files changed, 1759 insertions(+), 5 deletions(-) create mode 100644 includes/payment-methods/class-wc-stripe-express-checkout-element.php diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-element.php b/includes/payment-methods/class-wc-stripe-express-checkout-element.php new file mode 100644 index 000000000..8e5f1b905 --- /dev/null +++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php @@ -0,0 +1,1745 @@ +stripe_settings = WC_Stripe_Helper::get_stripe_settings(); + $this->testmode = ( ! empty( $this->stripe_settings['testmode'] ) && 'yes' === $this->stripe_settings['testmode'] ) ? true : false; + $this->total_label = ! empty( $this->stripe_settings['statement_descriptor'] ) ? WC_Stripe_Helper::clean_statement_descriptor( $this->stripe_settings['statement_descriptor'] ) : ''; + + $this->total_label = str_replace( "'", '', $this->total_label ) . apply_filters( 'wc_stripe_payment_request_total_label_suffix', ' (via WooCommerce)' ); + + $this->init(); + } + + /** + * Initialize hooks. + * + * @return void + */ + public function init() { + // Check if ECE feature flag is enabled. + if ( ! WC_Stripe_Feature_Flags::is_stripe_ece_enabled() ) { + return; + } + + // Checks if Stripe Gateway is enabled. + if ( empty( $this->stripe_settings ) || ( isset( $this->stripe_settings['enabled'] ) && 'yes' !== $this->stripe_settings['enabled'] ) ) { + return; + } + + // Don't initiate this class if express checkout element is disabled. + if ( ! $this->is_express_checkout_enabled() ) { + return; + } + + // Don't load for change payment method page. + if ( isset( $_GET['change_payment_method'] ) ) { + return; + } + + add_action( 'template_redirect', [ $this, 'set_session' ] ); + add_action( 'template_redirect', [ $this, 'handle_payment_request_redirect' ] ); + + add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); + + add_action( 'woocommerce_after_add_to_cart_form', [ $this, 'display_express_checkout_button_html' ], 1 ); + add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_express_checkout_button_html' ], 25 ); + add_action( 'woocommerce_checkout_before_customer_details', [ $this, 'display_express_checkout_button_html' ], 1 ); + + add_action( 'wc_ajax_wc_stripe_get_cart_details', [ $this, 'ajax_get_cart_details' ] ); + add_action( 'wc_ajax_wc_stripe_get_shipping_options', [ $this, 'ajax_get_shipping_options' ] ); + add_action( 'wc_ajax_wc_stripe_update_shipping_method', [ $this, 'ajax_update_shipping_method' ] ); + add_action( 'wc_ajax_wc_stripe_create_order', [ $this, 'ajax_create_order' ] ); + add_action( 'wc_ajax_wc_stripe_add_to_cart', [ $this, 'ajax_add_to_cart' ] ); + add_action( 'wc_ajax_wc_stripe_get_selected_product_data', [ $this, 'ajax_get_selected_product_data' ] ); + add_action( 'wc_ajax_wc_stripe_clear_cart', [ $this, 'ajax_clear_cart' ] ); + add_action( 'wc_ajax_wc_stripe_log_errors', [ $this, 'ajax_log_errors' ] ); + + add_filter( 'woocommerce_gateway_title', [ $this, 'filter_gateway_title' ], 10, 2 ); + add_action( 'woocommerce_checkout_order_processed', [ $this, 'add_order_meta' ], 10, 2 ); + add_filter( 'woocommerce_login_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); + add_filter( 'woocommerce_registration_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); + } + + /** + * Get this instance. + * + * @return class + */ + public static function instance() { + return self::$_this; + } + + /** + * Checks whether authentication is required for checkout. + * + * @return bool + */ + public function is_authentication_required() { + // If guest checkout is disabled and account creation upon checkout is not possible, authentication is required. + if ( 'no' === get_option( 'woocommerce_enable_guest_checkout', 'yes' ) && ! $this->is_account_creation_possible() ) { + return true; + } + // If cart contains subscription and account creation upon checkout is not posible, authentication is required. + if ( $this->has_subscription_product() && ! $this->is_account_creation_possible() ) { + return true; + } + + return false; + } + + /** + * Checks whether account creation is possible upon checkout. + * + * @return bool + */ + public function is_account_creation_possible() { + // If automatically generate username/password are disabled, we can not include any of those fields, + // during express checkout. So account creation is not possible. + return ( + 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'no' ) && + 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && + 'yes' === get_option( 'woocommerce_registration_generate_password', 'yes' ) + ); + } + + /** + * Sets the WC customer session if one is not set. + * This is needed so nonces can be verified by AJAX Request. + * + * @return void + */ + public function set_session() { + if ( ! $this->is_product() || ( isset( WC()->session ) && WC()->session->has_session() ) ) { + return; + } + + WC()->session->set_customer_session_cookie( true ); + } + + /** + * Handles express checkout redirect when the redirect dialog "Continue" button is clicked. + */ + public function handle_express_checkout_redirect() { + if ( + ! empty( $_GET['wc_stripe_express_checkout_redirect_url'] ) + && ! empty( $_GET['_wpnonce'] ) + && wp_verify_nonce( $_GET['_wpnonce'], 'wc-stripe-set-redirect-url' ) // @codingStandardsIgnoreLine + ) { + $url = rawurldecode( esc_url_raw( wp_unslash( $_GET['wc_stripe_express_checkout_redirect_url'] ) ) ); + // Sets a redirect URL cookie for 10 minutes, which we will redirect to after authentication. + // Users will have a 10 minute timeout to login/create account, otherwise redirect URL expires. + wc_setcookie( 'wc_stripe_express_checkout_redirect_url', $url, time() + MINUTE_IN_SECONDS * 10 ); + // Redirects to "my-account" page. + wp_safe_redirect( get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ) ); + exit; + } + } + + /** + * Gets the button type. + * + * @return string + */ + public function get_button_type() { + return isset( $this->stripe_settings['payment_request_button_type'] ) ? $this->stripe_settings['payment_request_button_type'] : 'default'; + } + + /** + * Gets the button theme. + * + * @return string + */ + public function get_button_theme() { + return isset( $this->stripe_settings['payment_request_button_theme'] ) ? $this->stripe_settings['payment_request_button_theme'] : 'dark'; + } + + /** + * Gets the button height. + * + * @return string + */ + public function get_button_height() { + $height = isset( $this->stripe_settings['payment_request_button_size'] ) ? $this->stripe_settings['payment_request_button_size'] : 'default'; + if ( 'small' === $height ) { + return '40'; + } + + if ( 'large' === $height ) { + return '56'; + } + + return '48'; + } + + /** + * Gets the product total price. + * + * @param object $product WC_Product_* object. + * @return integer Total price. + */ + public function get_product_price( $product ) { + $product_price = $product->get_price(); + // Add subscription sign-up fees to product price. + if ( in_array( $product->get_type(), [ 'subscription', 'subscription_variation' ] ) && class_exists( 'WC_Subscriptions_Product' ) ) { + $product_price = $product->get_price() + WC_Subscriptions_Product::get_sign_up_fee( $product ); + } + + return $product_price; + } + + /** + * Gets the product data for the currently viewed page + * + * @return mixed Returns false if not on a product page, the product information otherwise. + */ + public function get_product_data() { + if ( ! $this->is_product() ) { + return false; + } + + $product = $this->get_product(); + $variation_id = 0; + + if ( in_array( $product->get_type(), [ 'variable', 'variable-subscription' ], true ) ) { + $variation_attributes = $product->get_variation_attributes(); + $attributes = []; + + foreach ( $variation_attributes as $attribute_name => $attribute_values ) { + $attribute_key = 'attribute_' . sanitize_title( $attribute_name ); + + // Passed value via GET takes precedence, then POST, otherwise get the default value for given attribute + if ( isset( $_GET[ $attribute_key ] ) ) { + $attributes[ $attribute_key ] = wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ); + } elseif ( isset( $_POST[ $attribute_key ] ) ) { + $attributes[ $attribute_key ] = wc_clean( wp_unslash( $_POST[ $attribute_key ] ) ); + } else { + $attributes[ $attribute_key ] = $product->get_variation_default_attribute( $attribute_name ); + } + } + + $data_store = WC_Data_Store::load( 'product' ); + $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); + + if ( ! empty( $variation_id ) ) { + $product = wc_get_product( $variation_id ); + } + } + + $data = []; + $items = []; + + $items[] = [ + 'label' => $product->get_name(), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $this->get_product_price( $product ) ), + ]; + + if ( wc_tax_enabled() ) { + $items[] = [ + 'label' => __( 'Tax', 'woocommerce-gateway-stripe' ), + 'amount' => 0, + 'pending' => true, + ]; + } + + if ( wc_shipping_enabled() && $product->needs_shipping() ) { + $items[] = [ + 'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ), + 'amount' => 0, + 'pending' => true, + ]; + + $data['shippingOptions'] = [ + 'id' => 'pending', + 'label' => __( 'Pending', 'woocommerce-gateway-stripe' ), + 'detail' => '', + 'amount' => 0, + ]; + } + + $data['displayItems'] = $items; + $data['total'] = [ + 'label' => apply_filters( 'wc_stripe_payment_request_total_label', $this->total_label ), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $this->get_product_price( $product ) ), + ]; + + $data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() && 0 !== wc_get_shipping_method_count( true ) ); + $data['currency'] = strtolower( get_woocommerce_currency() ); + $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); + + // On product page load, if there's a variation already selected, check if it's supported. + $data['validVariationSelected'] = ! empty( $variation_id ) ? $this->is_product_supported( $product ) : true; + + return apply_filters( 'wc_stripe_payment_request_product_data', $data, $product ); + } + + /** + * Filters the gateway title to reflect express checkout type + */ + public function filter_gateway_title( $title, $id ) { + global $theorder; + + // If $theorder is empty (i.e. non-HPOS), fallback to using the global post object. + if ( empty( $theorder ) && ! empty( $GLOBALS['post']->ID ) ) { + $theorder = wc_get_order( $GLOBALS['post']->ID ); + } + + if ( ! is_object( $theorder ) ) { + return $title; + } + + $method_title = $theorder->get_payment_method_title(); + + if ( 'stripe' === $id && ! empty( $method_title ) ) { + if ( 'Apple Pay (Stripe)' === $method_title + || 'Google Pay (Stripe)' === $method_title + ) { + return $method_title; + } + } + + return $title; + } + + /** + * Normalizes postal code in case of redacted data from Apple Pay. + * + * @param string $postcode Postal code. + * @param string $country Country. + */ + public function get_normalized_postal_code( $postcode, $country ) { + /** + * Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively + * when passing it back from the shippingcontactselected object. This causes WC to invalidate + * the postal code and not calculate shipping zones correctly. + */ + if ( 'GB' === $country ) { + // Replaces a redacted string with something like LN10***. + return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '*' ); + } + if ( 'CA' === $country ) { + // Replaces a redacted string with something like L4Y***. + return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '*' ); + } + + return $postcode; + } + + /** + * Add needed order meta + * + * @param integer $order_id The order ID. + * @param array $posted_data The posted data from checkout form. + * + * @return void + */ + public function add_order_meta( $order_id, $posted_data ) { + if ( empty( $_POST['express_checkout_type'] ) || ! isset( $_POST['payment_method'] ) || 'stripe' !== $_POST['payment_method'] ) { + return; + } + + $order = wc_get_order( $order_id ); + + $express_checkout_type = wc_clean( wp_unslash( $_POST['express_checkout_type'] ) ); + + if ( 'apple_pay' === $express_checkout_type ) { + $order->set_payment_method_title( 'Apple Pay (Stripe)' ); + $order->save(); + } elseif ( 'google_pay' === $express_checkout_type ) { + $order->set_payment_method_title( 'Google Pay (Stripe)' ); + $order->save(); + } + } + + /** + * Checks to make sure product type is supported. + * + * @return array + */ + public function supported_product_types() { + return apply_filters( + 'wc_stripe_payment_request_supported_types', + [ + 'simple', + 'variable', + 'variation', + 'subscription', + 'variable-subscription', + 'subscription_variation', + 'booking', + 'bundle', + 'composite', + ] + ); + } + + /** + * Checks the cart to see if all items are allowed to be used. + * + * @return boolean + */ + public function allowed_items_in_cart() { + // Pre Orders compatibility where we don't support charge upon release. + if ( $this->is_pre_order_item_in_cart() && $this->is_pre_order_product_charged_upon_release( $this->get_pre_order_product_from_cart() ) ) { + return false; + } + + // If the cart is not available we don't have any unsupported products in the cart, so we + // return true. This can happen e.g. when loading the cart or checkout blocks in Gutenberg. + if ( is_null( WC()->cart ) ) { + return true; + } + + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); + + if ( ! in_array( $_product->get_type(), $this->supported_product_types() ) ) { + return false; + } + + // Subscriptions with a trial period that need shipping are not supported. + if ( $this->is_invalid_subscription_product( $_product ) ) { + return false; + } + } + + // We don't support multiple packages with express checkout buttons because we can't offer + // a good UX. + $packages = WC()->cart->get_shipping_packages(); + if ( 1 < count( $packages ) ) { + return false; + } + + return true; + } + + /** + * Returns true if the given product is a subscription that cannot be purchased with express checkout buttons. + * + * Invalid subscription products include those with: + * - a free trial that requires shipping (synchronised subscriptions with a delayed first payment are considered to have a free trial) + * - a synchronised subscription with no upfront payment and is virtual (this limitation only applies to the product page as we cannot calculate totals correctly) + * + * If the product is a variable subscription, this function will return true if all of its variations have a trial and require shipping. + * + * @since 7.8.0 + * + * @param WC_Product|null $product Product object. + * @param boolean $is_product_page_request Whether this is a request from the product page. + * + * @return boolean + */ + public function is_invalid_subscription_product( $product, $is_product_page_request = false ) { + if ( ! class_exists( 'WC_Subscriptions_Product' ) || ! class_exists( 'WC_Subscriptions_Synchroniser' ) || ! WC_Subscriptions_Product::is_subscription( $product ) ) { + return false; + } + + $is_invalid = true; + + if ( $product->get_type() === 'variable-subscription' ) { + $products = $product->get_available_variations( 'object' ); + } else { + $products = [ $product ]; + } + + foreach ( $products as $product ) { + $needs_shipping = $product->needs_shipping(); + $is_synced = WC_Subscriptions_Synchroniser::is_product_synced( $product ); + $is_payment_upfront = WC_Subscriptions_Synchroniser::is_payment_upfront( $product ); + $has_trial_period = WC_Subscriptions_Product::get_trial_length( $product ) > 0; + + if ( $is_product_page_request && $is_synced && ! $is_payment_upfront && ! $needs_shipping ) { + /** + * This condition prevents the purchase of virtual synced subscription products with no upfront costs via express checkout buttons from the product page. + * + * The main issue is that calling $product->get_price() on a synced subscription does not take into account a mock trial period or prorated price calculations + * until the product is in the cart. This means that the totals passed to express checkout element are incorrect when purchasing from the product page. + * Another part of the problem is because the product is virtual this stops the Stripe PaymentRequest API from triggering the necessary `shippingaddresschange` event + * which is when we call WC()->cart->calculate_totals(); which would fix the totals. + * + * The fix here is to not allow virtual synced subscription products with no upfront costs to be purchased via express checkout buttons on the product page. + */ + continue; + } elseif ( $is_synced && ! $is_payment_upfront && $needs_shipping ) { + continue; + } elseif ( $has_trial_period && $needs_shipping ) { + continue; + } else { + // If we made it this far, the product is valid. Break out of the foreach and return early as we only care about invalid cases. + $is_invalid = false; + break; + } + } + + return $is_invalid; + } + + /** + * Checks whether cart contains a subscription product or this is a subscription product page. + * + * @return boolean + */ + public function has_subscription_product() { + if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { + return false; + } + + if ( $this->is_product() ) { + $product = $this->get_product(); + if ( WC_Subscriptions_Product::is_subscription( $product ) ) { + return true; + } + } elseif ( WC_Stripe_Helper::has_cart_or_checkout_on_current_page() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); + if ( WC_Subscriptions_Product::is_subscription( $_product ) ) { + return true; + } + } + } + + return false; + } + + /** + * Checks if this is a product page or content contains a product_page shortcode. + * + * @return boolean + */ + public function is_product() { + return is_product() || wc_post_content_has_shortcode( 'product_page' ); + } + + /** + * Get product from product page or product_page shortcode. + * + * @return WC_Product Product object. + */ + public function get_product() { + global $post; + + if ( is_product() ) { + return wc_get_product( $post->ID ); + } elseif ( wc_post_content_has_shortcode( 'product_page' ) ) { + // Get id from product_page shortcode. + preg_match( '/\[product_page id="(?\d+)"\]/', $post->post_content, $shortcode_match ); + + if ( ! isset( $shortcode_match['id'] ) ) { + return false; + } + + return wc_get_product( $shortcode_match['id'] ); + } + + return false; + } + + /** + * Returns the login redirect URL. + * + * @param string $redirect Default redirect URL. + * @return string Redirect URL. + */ + public function get_login_redirect_url( $redirect ) { + $url = esc_url_raw( wp_unslash( isset( $_COOKIE['wc_stripe_express_checkout_redirect_url'] ) ? $_COOKIE['wc_stripe_payment_request_redirect_url'] : '' ) ); + + if ( empty( $url ) ) { + return $redirect; + } + wc_setcookie( 'wc_stripe_express_checkout_redirect_url', null ); + + return $url; + } + + /** + * Returns the JavaScript configuration object used for any pages with express checkout element. + * + * @return array The settings used for the Stripe express checkout element in JavaScript. + */ + public function javascript_params() { + $needs_shipping = 'no'; + if ( ! is_null( WC()->cart ) && WC()->cart->needs_shipping() ) { + $needs_shipping = 'yes'; + } + + return [ + 'ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'stripe' => [ + 'allow_prepaid_card' => apply_filters( 'wc_stripe_allow_prepaid_card', true ) ? 'yes' : 'no', + 'locale' => WC_Stripe_Helper::convert_wc_locale_to_stripe_locale( get_locale() ), + 'is_link_enabled' => WC_Stripe_UPE_Payment_Method_Link::is_link_enabled(), + 'is_express_checkout_enabled' => $this->is_express_checkout_enabled(), + ], + 'nonce' => [ + 'payment' => wp_create_nonce( 'wc-stripe-express-checkout-element' ), + 'shipping' => wp_create_nonce( 'wc-stripe-express-checkout-element-shipping' ), + 'update_shipping' => wp_create_nonce( 'wc-stripe-update-shipping-method' ), + 'checkout' => wp_create_nonce( 'woocommerce-process_checkout' ), + 'add_to_cart' => wp_create_nonce( 'wc-stripe-add-to-cart' ), + 'get_selected_product_data' => wp_create_nonce( 'wc-stripe-get-selected-product-data' ), + 'log_errors' => wp_create_nonce( 'wc-stripe-log-errors' ), + 'clear_cart' => wp_create_nonce( 'wc-stripe-clear-cart' ), + ], + 'i18n' => [ + 'no_prepaid_card' => __( 'Sorry, we\'re not accepting prepaid cards at this time.', 'woocommerce-gateway-stripe' ), + /* translators: Do not translate the [option] placeholder */ + 'unknown_shipping' => __( 'Unknown shipping option "[option]".', 'woocommerce-gateway-stripe' ), + ], + 'checkout' => [ + 'url' => wc_get_checkout_url(), + 'currency_code' => strtolower( get_woocommerce_currency() ), + 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ), + 'needs_shipping' => $needs_shipping, + // Defaults to 'required' to match how core initializes this option. + 'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), + ], + 'button' => $this->get_button_settings(), + 'login_confirmation' => $this->get_login_confirmation_settings(), + 'is_product_page' => $this->is_product(), + 'product' => $this->get_product_data(), + ]; + } + + /** + * Load scripts and styles. + */ + public function scripts() { + // If page is not supported, bail. + if ( ! $this->is_page_supported() ) { + return; + } + + if ( ! $this->should_show_express_checkout_button() ) { + return; + } + + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + + wp_register_script( 'stripe', 'https://js.stripe.com/v3/', '', '3.0', true ); + wp_register_script( 'wc_stripe_express_checkout', plugins_url( 'assets/js/stripe-express_checkout' . $suffix . '.js', WC_STRIPE_MAIN_FILE ), [ 'jquery', 'stripe' ], WC_STRIPE_VERSION, true ); + + wp_localize_script( + 'wc_stripe_express_checkout', + 'wc_stripe_express_checkout_params', + apply_filters( + 'wc_stripe_express_checkout_params', + $this->javascript_params() + ) + ); + + wp_enqueue_script( 'wc_stripe_express_checkout' ); + } + + /** + * Returns true if the current page supports Express Checkout Buttons, false otherwise. + * + * @return boolean True if the current page is supported, false otherwise. + */ + private function is_page_supported() { + return $this->is_product() + || WC_Stripe_Helper::has_cart_or_checkout_on_current_page() + || is_wc_endpoint_url( 'order-pay' ); + } + + /** + * Display the express checkout button. + */ + public function display_express_checkout_button_html() { + $gateways = WC()->payment_gateways->get_available_payment_gateways(); + + if ( ! isset( $gateways['stripe'] ) ) { + return; + } + + if ( ! $this->is_page_supported() ) { + return; + } + + if ( ! $this->should_show_express_checkout_button() ) { + return; + } + + ?> + + display_express_checkout_button_separator_html(); + } + + /** + * Display express checkout button separator. + */ + public function display_express_checkout_button_separator_html() { + if ( ! is_checkout() && ! is_wc_endpoint_url( 'order-pay' ) ) { + return; + } + + if ( is_checkout() && ! in_array( 'checkout', $this->get_button_locations(), true ) ) { + return; + } + ?> + + connect->is_connected() ) { + WC_Stripe_Logger::log( 'Account is not connected.' ); + return false; + } + + // If no SSL bail. + if ( ! $this->testmode && ! is_ssl() ) { + WC_Stripe_Logger::log( 'Stripe Express Checkout live mode requires SSL.' ); + return false; + } + + // Don't show if on the cart or checkout page, or if page contains the cart or checkout + // shortcodes, with items in the cart that aren't supported. + if ( + WC_Stripe_Helper::has_cart_or_checkout_on_current_page() + && ! $this->allowed_items_in_cart() + ) { + return false; + } + + // Don't show on cart if disabled. + if ( is_cart() && ! $this->should_show_ece_on_cart_page() ) { + return false; + } + + // Don't show on checkout if disabled. + if ( is_checkout() && ! $this->should_show_ece_on_checkout_page() ) { + return false; + } + + // Don't show if product page PRB is disabled. + if ( $this->is_product() && ! $this->should_show_ece_on_product_pages() ) { + return false; + } + + // Don't show if product on current page is not supported. + if ( $this->is_product() && ! $this->is_product_supported( $this->get_product() ) ) { + return false; + } + + if ( $this->is_product() && in_array( $this->get_product()->get_type(), [ 'variable', 'variable-subscription' ], true ) ) { + $stock_availability = array_column( $this->get_product()->get_available_variations(), 'is_in_stock' ); + // Don't show if all product variations are out-of-stock. + if ( ! in_array( true, $stock_availability, true ) ) { + return false; + } + } + + return true; + } + + /** + * Returns true if express checkout buttons are enabled on the cart page, false + * otherwise. + * + * @return boolean True if express checkout buttons are enabled on the cart page, false otherwise + */ + public function should_show_ece_on_cart_page() { + $should_show_on_cart_page = in_array( 'cart', $this->get_button_locations(), true ); + + return apply_filters( + 'wc_stripe_show_payment_request_on_cart', + $should_show_on_cart_page + ); + } + + /** + * Returns true if express checkout buttons are enabled on the checkout page, false + * otherwise. + * + * @return boolean True if express checkout buttons are enabled on the checkout page, false otherwise + */ + public function should_show_ece_on_checkout_page() { + global $post; + + $should_show_on_checkout_page = in_array( 'checkout', $this->get_button_locations(), true ); + + return apply_filters( + 'wc_stripe_show_payment_request_on_checkout', + $should_show_on_checkout_page, + $post + ); + } + + /** + * Returns true if express checkout buttons are enabled on product pages, false + * otherwise. + * + * @return boolean True if express checkout buttons are enabled on product pages, false otherwise + */ + public function should_show_ece_on_product_pages() { + global $post; + + $should_show_on_product_page = in_array( 'product', $this->get_button_locations(), true ); + + // Note the negation because if the filter returns `true` that means we should hide the PRB. + return ! apply_filters( + 'wc_stripe_hide_payment_request_on_product_page', + ! $should_show_on_product_page, + $post + ); + } + + /** + * Returns true if a the provided product is supported, false otherwise. + * + * @param WC_Product $param The product that's being checked for support. + * + * @return boolean True if the provided product is supported, false otherwise. + */ + private function is_product_supported( $product ) { + if ( ! is_object( $product ) || ! in_array( $product->get_type(), $this->supported_product_types() ) ) { + return false; + } + + // Trial subscriptions with shipping are not supported. + if ( $this->is_invalid_subscription_product( $product, true ) ) { + return false; + } + + // Pre Orders charge upon release not supported. + if ( $this->is_pre_order_product_charged_upon_release( $product ) ) { + return false; + } + + // Composite products are not supported on the product page. + if ( class_exists( 'WC_Composite_Products' ) && function_exists( 'is_composite_product' ) && is_composite_product() ) { + return false; + } + + // File upload addon not supported + if ( class_exists( 'WC_Product_Addons_Helper' ) ) { + $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() ); + foreach ( $product_addons as $addon ) { + if ( 'file_upload' === $addon['type'] ) { + return false; + } + } + } + + return true; + } + + /** + * Log errors coming from express checkout elements + */ + public function ajax_log_errors() { + check_ajax_referer( 'wc-stripe-log-errors', 'security' ); + + $errors = isset( $_POST['errors'] ) ? wc_clean( wp_unslash( $_POST['errors'] ) ) : ''; + + WC_Stripe_Logger::log( $errors ); + + exit; + } + + /** + * Clears cart. + */ + public function ajax_clear_cart() { + check_ajax_referer( 'wc-stripe-clear-cart', 'security' ); + + WC()->cart->empty_cart(); + exit; + } + + /** + * Get cart details. + */ + public function ajax_get_cart_details() { + check_ajax_referer( 'wc-stripe-express-checkout', 'security' ); + + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + WC()->cart->calculate_totals(); + + $currency = get_woocommerce_currency(); + + // Set mandatory payment details. + $data = [ + 'shipping_required' => WC()->cart->needs_shipping(), + 'order_data' => [ + 'currency' => strtolower( $currency ), + 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ), + ], + ]; + + $data['order_data'] += $this->build_display_items(); + + wp_send_json( $data ); + } + + /** + * Get shipping options. + * + * @see WC_Cart::get_shipping_packages(). + * @see WC_Shipping::calculate_shipping(). + * @see WC_Shipping::get_packages(). + */ + public function ajax_get_shipping_options() { + check_ajax_referer( 'wc-stripe-express-checkout-shipping', 'security' ); + + $shipping_address = filter_input_array( + INPUT_POST, + [ + 'country' => FILTER_SANITIZE_SPECIAL_CHARS, + 'state' => FILTER_SANITIZE_SPECIAL_CHARS, + 'postcode' => FILTER_SANITIZE_SPECIAL_CHARS, + 'city' => FILTER_SANITIZE_SPECIAL_CHARS, + 'address' => FILTER_SANITIZE_SPECIAL_CHARS, + 'address_2' => FILTER_SANITIZE_SPECIAL_CHARS, + ] + ); + $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] ); + $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); + + $data = $this->get_shipping_options( $shipping_address, $should_show_itemized_view ); + wp_send_json( $data ); + } + + /** + * Gets shipping options available for specified shipping address + * + * @param array $shipping_address Shipping address. + * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views. + * + * @return array Shipping options data. + * + * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag + */ + public function get_shipping_options( $shipping_address, $itemized_display_items = false ) { + try { + // Set the shipping options. + $data = []; + + // Remember current shipping method before resetting. + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); + $this->calculate_shipping( apply_filters( 'wc_stripe_payment_request_shipping_posted_values', $shipping_address ) ); + + $packages = WC()->shipping->get_packages(); + $shipping_rate_ids = []; + + if ( ! empty( $packages ) && WC()->customer->has_calculated_shipping() ) { + foreach ( $packages as $package_key => $package ) { + if ( empty( $package['rates'] ) ) { + throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-gateway-stripe' ) ); + } + + foreach ( $package['rates'] as $key => $rate ) { + if ( in_array( $rate->id, $shipping_rate_ids, true ) ) { + // The express checkout will try to load indefinitely if there are duplicate shipping + // option IDs. + throw new Exception( __( 'Unable to provide shipping options for express checkout.', 'woocommerce-gateway-stripe' ) ); + } + $shipping_rate_ids[] = $rate->id; + $data['shipping_options'][] = [ + 'id' => $rate->id, + 'label' => $rate->label, + 'detail' => '', + 'amount' => WC_Stripe_Helper::get_stripe_amount( $rate->cost ), + ]; + } + } + } else { + throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-gateway-stripe' ) ); + } + + // The first shipping option is automatically applied on the client. + // Keep chosen shipping method by sorting shipping options if the method still available for new address. + // Fallback to the first available shipping method. + if ( isset( $data['shipping_options'][0] ) ) { + if ( isset( $chosen_shipping_methods[0] ) ) { + $chosen_method_id = $chosen_shipping_methods[0]; + $compare_shipping_options = function ( $a, $b ) use ( $chosen_method_id ) { + if ( $a['id'] === $chosen_method_id ) { + return -1; + } + + if ( $b['id'] === $chosen_method_id ) { + return 1; + } + + return 0; + }; + usort( $data['shipping_options'], $compare_shipping_options ); + } + + $first_shipping_method_id = $data['shipping_options'][0]['id']; + $this->update_shipping_method( [ $first_shipping_method_id ] ); + } + + WC()->cart->calculate_totals(); + + $this->maybe_restore_recurring_chosen_shipping_methods( $chosen_shipping_methods ); + + $data += $this->build_display_items( $itemized_display_items ); + $data['result'] = 'success'; + } catch ( Exception $e ) { + $data += $this->build_display_items( $itemized_display_items ); + $data['result'] = 'invalid_shipping_address'; + } + + return $data; + } + + /** + * Update shipping method. + */ + public function ajax_update_shipping_method() { + check_ajax_referer( 'wc-stripe-update-shipping-method', 'security' ); + + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + $shipping_methods = filter_input( INPUT_POST, 'shipping_method', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); + $this->update_shipping_method( $shipping_methods ); + + WC()->cart->calculate_totals(); + + $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] ); + $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); + + $data = []; + $data += $this->build_display_items( $should_show_itemized_view ); + $data['result'] = 'success'; + + wp_send_json( $data ); + } + + /** + * Updates shipping method in WC session + * + * @param array $shipping_methods Array of selected shipping methods ids. + */ + public function update_shipping_method( $shipping_methods ) { + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); + + if ( is_array( $shipping_methods ) ) { + foreach ( $shipping_methods as $i => $value ) { + $chosen_shipping_methods[ $i ] = wc_clean( $value ); + } + } + + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } + + /** + * Gets the selected product data. + * + * @return array $data The selected product data. + */ + public function ajax_get_selected_product_data() { + check_ajax_referer( 'wc-stripe-get-selected-product-data', 'security' ); + + try { // @phpstan-ignore-line (return statement is added) + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; + $qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id ); + $addon_value = isset( $_POST['addon_value'] ) ? max( floatval( $_POST['addon_value'] ), 0 ) : 0; + $product = wc_get_product( $product_id ); + $variation_id = null; + + if ( ! is_a( $product, 'WC_Product' ) ) { + /* translators: 1) The product Id */ + throw new Exception( sprintf( __( 'Product with the ID (%1$s) cannot be found.', 'woocommerce-gateway-stripe' ), $product_id ) ); + } + + if ( in_array( $product->get_type(), [ 'variable', 'variable-subscription' ], true ) && isset( $_POST['attributes'] ) ) { + $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); + + $data_store = WC_Data_Store::load( 'product' ); + $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); + + if ( ! empty( $variation_id ) ) { + $product = wc_get_product( $variation_id ); + } + } + + if ( $this->is_invalid_subscription_product( $product, true ) ) { + throw new Exception( __( 'The chosen subscription product is not supported.', 'woocommerce-gateway-stripe' ) ); + } + + // Force quantity to 1 if sold individually and check for existing item in cart. + if ( $product->is_sold_individually() ) { + $qty = apply_filters( 'wc_stripe_payment_request_add_to_cart_sold_individually_quantity', 1, $qty, $product_id, $variation_id ); + } + + if ( ! $product->has_enough_stock( $qty ) ) { + /* translators: 1) product name 2) quantity in stock */ + throw new Exception( sprintf( __( 'You cannot add that amount of "%1$s"; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce-gateway-stripe' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) ); + } + + $total = $qty * $this->get_product_price( $product ) + $addon_value; + + $quantity_label = 1 < $qty ? ' (x' . $qty . ')' : ''; + + $data = []; + $items = []; + + $items[] = [ + 'label' => $product->get_name() . $quantity_label, + 'amount' => WC_Stripe_Helper::get_stripe_amount( $total ), + ]; + + if ( wc_tax_enabled() ) { + $items[] = [ + 'label' => __( 'Tax', 'woocommerce-gateway-stripe' ), + 'amount' => 0, + 'pending' => true, + ]; + } + + if ( wc_shipping_enabled() && $product->needs_shipping() ) { + $items[] = [ + 'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ), + 'amount' => 0, + 'pending' => true, + ]; + + $data['shippingOptions'] = [ + 'id' => 'pending', + 'label' => __( 'Pending', 'woocommerce-gateway-stripe' ), + 'detail' => '', + 'amount' => 0, + ]; + } + + $data['displayItems'] = $items; + $data['total'] = [ + 'label' => $this->total_label, + 'amount' => WC_Stripe_Helper::get_stripe_amount( $total ), + ]; + + $data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() ); + $data['currency'] = strtolower( get_woocommerce_currency() ); + $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); + + wp_send_json( $data ); + } catch ( Exception $e ) { + wp_send_json( [ 'error' => wp_strip_all_tags( $e->getMessage() ) ] ); + } + } + + /** + * Adds the current product to the cart. Used on product detail page. + * + * @return array $data Results of adding the product to the cart. + */ + public function ajax_add_to_cart() { + check_ajax_referer( 'wc-stripe-add-to-cart', 'security' ); + + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + WC()->shipping->reset_shipping(); + + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; + $qty = ! isset( $_POST['qty'] ) ? 1 : absint( $_POST['qty'] ); + $product = wc_get_product( $product_id ); + $product_type = $product->get_type(); + + // First empty the cart to prevent wrong calculation. + WC()->cart->empty_cart(); + + if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) { + $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); + + $data_store = WC_Data_Store::load( 'product' ); + $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); + + WC()->cart->add_to_cart( $product->get_id(), $qty, $variation_id, $attributes ); + } + + if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation' ], true ) ) { + WC()->cart->add_to_cart( $product->get_id(), $qty ); + } + + WC()->cart->calculate_totals(); + + $data = []; + $data += $this->build_display_items(); + $data['result'] = 'success'; + + // @phpstan-ignore-next-line (return statement is added) + wp_send_json( $data ); + } + + /** + * Normalizes billing and shipping state fields. + */ + public function normalize_state() { + $billing_country = ! empty( $_POST['billing_country'] ) ? wc_clean( wp_unslash( $_POST['billing_country'] ) ) : ''; + $shipping_country = ! empty( $_POST['shipping_country'] ) ? wc_clean( wp_unslash( $_POST['shipping_country'] ) ) : ''; + $billing_state = ! empty( $_POST['billing_state'] ) ? wc_clean( wp_unslash( $_POST['billing_state'] ) ) : ''; + $shipping_state = ! empty( $_POST['shipping_state'] ) ? wc_clean( wp_unslash( $_POST['shipping_state'] ) ) : ''; + + // Due to a bug in Apple Pay, the "Region" part of a Hong Kong address is delivered in + // `shipping_postcode`, so we need some special case handling for that. According to + // our sources at Apple Pay people will sometimes use the district or even sub-district + // for this value. As such we check against all regions, districts, and sub-districts + // with both English and Mandarin spelling. + // + // @reykjalin: The check here is quite elaborate in an attempt to make sure this doesn't break once + // Apple Pay fixes the bug that causes address values to be in the wrong place. Because of that the + // algorithm becomes: + // 1. Use the supplied state if it's valid (in case Apple Pay bug is fixed) + // 2. Use the value supplied in the postcode if it's a valid HK region (equivalent to a WC state). + // 3. Fall back to the value supplied in the state. This will likely cause a validation error, in + // which case a merchant can reach out to us so we can either: 1) add whatever the customer used + // as a state to our list of valid states; or 2) let them know the customer must spell the state + // in some way that matches our list of valid states. + // + // @reykjalin: This HK specific sanitazation *should be removed* once Apple Pay fix + // the address bug. More info on that in pc4etw-bY-p2. + if ( 'HK' === $billing_country ) { + include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-hong-kong-states.php'; + + if ( ! WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $billing_state ) ) ) { + $billing_postcode = ! empty( $_POST['billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['billing_postcode'] ) ) : ''; + if ( WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $billing_postcode ) ) ) { + $billing_state = $billing_postcode; + } + } + } + if ( 'HK' === $shipping_country ) { + include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-hong-kong-states.php'; + + if ( ! WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $shipping_state ) ) ) { + $shipping_postcode = ! empty( $_POST['shipping_postcode'] ) ? wc_clean( wp_unslash( $_POST['shipping_postcode'] ) ) : ''; + if ( WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $shipping_postcode ) ) ) { + $shipping_state = $shipping_postcode; + } + } + } + + // Finally we normalize the state value we want to process. + if ( $billing_state && $billing_country ) { + $_POST['billing_state'] = $this->get_normalized_state( $billing_state, $billing_country ); + } + + if ( $shipping_state && $shipping_country ) { + $_POST['shipping_state'] = $this->get_normalized_state( $shipping_state, $shipping_country ); + } + } + + /** + * Checks if given state is normalized. + * + * @param string $state State. + * @param string $country Two-letter country code. + * + * @return bool Whether state is normalized or not. + */ + public function is_normalized_state( $state, $country ) { + $wc_states = WC()->countries->get_states( $country ); + return ( + is_array( $wc_states ) && + in_array( $state, array_keys( $wc_states ), true ) + ); + } + + /** + * Sanitize string for comparison. + * + * @param string $string String to be sanitized. + * + * @return string The sanitized string. + */ + public function sanitize_string( $string ) { + return trim( wc_strtolower( remove_accents( $string ) ) ); + } + + /** + * Get normalized state from express checkout API dropdown list of states. + * + * @param string $state Full state name or state code. + * @param string $country Two-letter country code. + * + * @return string Normalized state or original state input value. + */ + public function get_normalized_state_from_pr_states( $state, $country ) { + // Include Payment Request API State list for compatibility with WC countries/states. + include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-payment-request-button-states.php'; + $pr_states = WC_Stripe_Payment_Request_Button_States::STATES; + + if ( ! isset( $pr_states[ $country ] ) ) { + return $state; + } + + foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) { + $sanitized_state_string = $this->sanitize_string( $state ); + // Checks if input state matches with Payment Request state code (0), name (1) or localName (2). + if ( + ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) || + ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) || + ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) ) + ) { + return $wc_state_abbr; + } + } + + return $state; + } + + /** + * Get normalized state from WooCommerce list of translated states. + * + * @param string $state Full state name or state code. + * @param string $country Two-letter country code. + * + * @return string Normalized state or original state input value. + */ + public function get_normalized_state_from_wc_states( $state, $country ) { + $wc_states = WC()->countries->get_states( $country ); + + if ( is_array( $wc_states ) ) { + foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) { + if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) { + return $wc_state_abbr; + } + } + } + + return $state; + } + + /** + * Gets the normalized state/county field because in some + * cases, the state/county field is formatted differently from + * what WC is expecting and throws an error. An example + * for Ireland, the county dropdown in Chrome shows "Co. Clare" format. + * + * @param string $state Full state name or an already normalized abbreviation. + * @param string $country Two-letter country code. + * + * @return string Normalized state abbreviation. + */ + public function get_normalized_state( $state, $country ) { + // If it's empty or already normalized, skip. + if ( ! $state || $this->is_normalized_state( $state, $country ) ) { + return $state; + } + + // Try to match state from the Payment Request API list of states. + $state = $this->get_normalized_state_from_pr_states( $state, $country ); + + // If it's normalized, return. + if ( $this->is_normalized_state( $state, $country ) ) { + return $state; + } + + // If the above doesn't work, fallback to matching against the list of translated + // states from WooCommerce. + return $this->get_normalized_state_from_wc_states( $state, $country ); + } + + /** + * The express checkout API provides its own validation for the address form. + * For some countries, it might not provide a state field, so we need to return a more descriptive + * error message, indicating that the express checkout button is not supported for that country. + */ + public function validate_state() { + $wc_checkout = WC_Checkout::instance(); + $posted_data = $wc_checkout->get_posted_data(); + $checkout_fields = $wc_checkout->get_checkout_fields(); + $countries = WC()->countries->get_countries(); + + $is_supported = true; + // Checks if billing state is missing and is required. + if ( ! empty( $checkout_fields['billing']['billing_state']['required'] ) && '' === $posted_data['billing_state'] ) { + $is_supported = false; + } + + // Checks if shipping state is missing and is required. + if ( WC()->cart->needs_shipping_address() && ! empty( $checkout_fields['shipping']['shipping_state']['required'] ) && '' === $posted_data['shipping_state'] ) { + $is_supported = false; + } + + if ( ! $is_supported ) { + wc_add_notice( + sprintf( + /* translators: 1) country. */ + __( 'The Express Checkout button is not supported in %1$s because some required fields couldn\'t be verified. Please proceed to the checkout page and try again.', 'woocommerce-gateway-stripe' ), + isset( $countries[ $posted_data['billing_country'] ] ) ? $countries[ $posted_data['billing_country'] ] : $posted_data['billing_country'] + ), + 'error' + ); + } + } + + /** + * Create order. Security is handled by WC. + */ + public function ajax_create_order() { + if ( WC()->cart->is_empty() ) { + wp_send_json_error( __( 'Empty cart', 'woocommerce-gateway-stripe' ) ); + } + + if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { + define( 'WOOCOMMERCE_CHECKOUT', true ); + } + + // Normalizes billing and shipping state values. + $this->normalize_state(); + + // In case the state is required, but is missing, add a more descriptive error notice. + $this->validate_state(); + + WC()->checkout()->process_checkout(); + + die( 0 ); + } + + /** + * Calculate and set shipping method. + * + * @param array $address Shipping address. + */ + protected function calculate_shipping( $address = [] ) { + $country = $address['country']; + $state = $address['state']; + $postcode = $address['postcode']; + $city = $address['city']; + $address_1 = $address['address']; + $address_2 = $address['address_2']; + + // Normalizes state to calculate shipping zones. + $state = $this->get_normalized_state( $state, $country ); + + // Normalizes postal code in case of redacted data from Apple Pay. + $postcode = $this->get_normalized_postal_code( $postcode, $country ); + + WC()->shipping->reset_shipping(); + + if ( $postcode && WC_Validation::is_postcode( $postcode, $country ) ) { + $postcode = wc_format_postcode( $postcode, $country ); + } + + if ( $country ) { + WC()->customer->set_location( $country, $state, $postcode, $city ); + WC()->customer->set_shipping_location( $country, $state, $postcode, $city ); + } else { + WC()->customer->set_billing_address_to_base(); + WC()->customer->set_shipping_address_to_base(); + } + + WC()->customer->set_calculated_shipping( true ); + WC()->customer->save(); + + $packages = []; + + $packages[0]['contents'] = WC()->cart->get_cart(); + $packages[0]['contents_cost'] = 0; + $packages[0]['applied_coupons'] = WC()->cart->applied_coupons; + $packages[0]['user']['ID'] = get_current_user_id(); + $packages[0]['destination']['country'] = $country; + $packages[0]['destination']['state'] = $state; + $packages[0]['destination']['postcode'] = $postcode; + $packages[0]['destination']['city'] = $city; + $packages[0]['destination']['address'] = $address_1; + $packages[0]['destination']['address_2'] = $address_2; + + foreach ( WC()->cart->get_cart() as $item ) { + if ( $item['data']->needs_shipping() ) { + if ( isset( $item['line_total'] ) ) { + $packages[0]['contents_cost'] += $item['line_total']; + } + } + } + + $packages = apply_filters( 'woocommerce_cart_shipping_packages', $packages ); + + WC()->shipping->calculate_shipping( $packages ); + } + + /** + * The settings for the `button` attribute. + * + * @return array + */ + public function get_button_settings() { + $button_type = $this->get_button_type(); + return [ + 'type' => $button_type, + 'theme' => $this->get_button_theme(), + 'height' => $this->get_button_height(), + // Default format is en_US. + 'locale' => apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) ), + ]; + } + + /** + * Builds the shippings methods to pass to express checkout elements. + */ + protected function build_shipping_methods( $shipping_methods ) { + if ( empty( $shipping_methods ) ) { + return []; + } + + $shipping = []; + + foreach ( $shipping_methods as $method ) { + $shipping[] = [ + 'id' => $method['id'], + 'label' => $method['label'], + 'detail' => '', + 'amount' => WC_Stripe_Helper::get_stripe_amount( $method['amount']['value'] ), + ]; + } + + return $shipping; + } + + /** + * Builds the line items to pass to express checkout elements. + */ + protected function build_display_items( $itemized_display_items = false ) { + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + $items = []; + $lines = []; + $subtotal = 0; + $discounts = 0; + $display_items = ! apply_filters( 'wc_stripe_payment_request_hide_itemization', true ) || $itemized_display_items; + + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $subtotal += $cart_item['line_subtotal']; + $amount = $cart_item['line_subtotal']; + $quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : ''; + $product_name = $cart_item['data']->get_name(); + + $lines[] = [ + 'label' => $product_name . $quantity_label, + 'amount' => WC_Stripe_Helper::get_stripe_amount( $amount ), + ]; + } + + if ( $display_items ) { + $items = array_merge( $items, $lines ); + } else { + // Default show only subtotal instead of itemization. + + $items[] = [ + 'label' => 'Subtotal', + 'amount' => WC_Stripe_Helper::get_stripe_amount( $subtotal ), + ]; + } + + $applied_coupons = array_values( WC()->cart->get_coupon_discount_totals() ); + + foreach ( $applied_coupons as $amount ) { + $discounts += (float) $amount; + } + + $discounts = wc_format_decimal( $discounts, WC()->cart->dp ); + $tax = wc_format_decimal( WC()->cart->tax_total + WC()->cart->shipping_tax_total, WC()->cart->dp ); + $shipping = wc_format_decimal( WC()->cart->shipping_total, WC()->cart->dp ); + $items_total = wc_format_decimal( WC()->cart->cart_contents_total, WC()->cart->dp ) + $discounts; + $order_total = WC()->cart->get_total( false ); + + if ( wc_tax_enabled() ) { + $items[] = [ + 'label' => esc_html( __( 'Tax', 'woocommerce-gateway-stripe' ) ), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $tax ), + ]; + } + + if ( WC()->cart->needs_shipping() ) { + $items[] = [ + 'label' => esc_html( __( 'Shipping', 'woocommerce-gateway-stripe' ) ), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $shipping ), + ]; + } + + if ( WC()->cart->has_discount() ) { + $items[] = [ + 'label' => esc_html( __( 'Discount', 'woocommerce-gateway-stripe' ) ), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $discounts ), + ]; + } + + $cart_fees = WC()->cart->get_fees(); + + // Include fees and taxes as display items. + foreach ( $cart_fees as $key => $fee ) { + $items[] = [ + 'label' => $fee->name, + 'amount' => WC_Stripe_Helper::get_stripe_amount( $fee->amount ), + ]; + } + + return [ + 'displayItems' => $items, + 'total' => [ + 'label' => $this->total_label, + 'amount' => max( 0, apply_filters( 'woocommerce_stripe_calculated_total', WC_Stripe_Helper::get_stripe_amount( $order_total ), $order_total, WC()->cart ) ), + 'pending' => false, + ], + ]; + } + + /** + * Settings array for the user authentication dialog and redirection. + * + * @return array + */ + public function get_login_confirmation_settings() { + if ( is_user_logged_in() || ! $this->is_authentication_required() ) { + return false; + } + + /* translators: The text encapsulated in `**` can be replaced with "Apple Pay" or "Google Pay". Please translate this text, but don't remove the `**`. */ + $message = __( 'To complete your transaction with **the selected payment method**, you must log in or create an account with our site.', 'woocommerce-gateway-stripe' ); + $redirect_url = add_query_arg( + [ + '_wpnonce' => wp_create_nonce( 'wc-stripe-set-redirect-url' ), + 'wc_stripe_express_checkout_redirect_url' => rawurlencode( home_url( add_query_arg( [] ) ) ), // Current URL to redirect to after login. + ], + home_url() + ); + + return [ + 'message' => $message, + 'redirect_url' => wp_sanitize_redirect( esc_url_raw( $redirect_url ) ), + ]; + } + + /** + * Pages where the express checkout buttons should be displayed. + * + * @return array + */ + public function get_button_locations() { + // If the locations have not been set return the default setting. + if ( ! isset( $this->stripe_settings['payment_request_button_locations'] ) ) { + return [ 'product', 'cart' ]; + } + + // If all locations are removed through the settings UI the location config will be set to + // an empty string "". If that's the case (and if the settings are not an array for any + // other reason) we should return an empty array. + if ( ! is_array( $this->stripe_settings['payment_request_button_locations'] ) ) { + return []; + } + + return $this->stripe_settings['payment_request_button_locations']; + } + + /** + * Returns whether Stripe express checkout element is enabled. + * + * This option defines whether Apple Pay and Google Pay buttons are enabled. + * + * @return boolean + */ + private function is_express_checkout_enabled() { + return isset( $this->stripe_settings['payment_request'] ) && 'yes' === $this->stripe_settings['payment_request']; + } + + /** + * Restores the shipping methods previously chosen for each recurring cart after shipping was reset and recalculated + * during the express checkout get_shipping_options flow. + * + * When the cart contains multiple subscriptions with different billing periods, customers are able to select different shipping + * methods for each subscription, however, this is not supported when purchasing with Apple Pay and Google Pay as it's + * only concerned about handling the initial purchase. + * + * In order to avoid Woo Subscriptions's `WC_Subscriptions_Cart::validate_recurring_shipping_methods` throwing an error, we need to restore + * the previously chosen shipping methods for each recurring cart. + * + * This function needs to be called after `WC()->cart->calculate_totals()` is run, otherwise `WC()->cart->recurring_carts` won't exist yet. + * + * @param array $previous_chosen_methods The previously chosen shipping methods. + */ + private function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) { + if ( empty( WC()->cart->recurring_carts ) || ! method_exists( 'WC_Subscriptions_Cart', 'get_recurring_shipping_package_key' ) ) { + return; + } + + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); + + foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) { + foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_index => $recurring_cart_package ) { + if ( class_exists( 'WC_Subscriptions_Cart' ) ) { + $package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $recurring_cart_key, $recurring_cart_package_index ); + + // If the recurring cart package key is found in the previous chosen methods, but not in the current chosen methods, restore it. + if ( isset( $previous_chosen_methods[ $package_key ] ) && ! isset( $chosen_shipping_methods[ $package_key ] ) ) { + $chosen_shipping_methods[ $package_key ] = $previous_chosen_methods[ $package_key ]; + } + } + } + } + + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } +} diff --git a/woocommerce-gateway-stripe.php b/woocommerce-gateway-stripe.php index 2ce2cee14..e4c4a1ab2 100644 --- a/woocommerce-gateway-stripe.php +++ b/woocommerce-gateway-stripe.php @@ -131,6 +131,13 @@ public static function get_instance() { */ public $payment_request_configuration; + /** + * Stripe Express Checkout configurations. + * + * @var WC_Stripe_Express_Checkout + */ + public $express_checkout_configuration; + /** * Stripe Account. * @@ -237,6 +244,7 @@ public function init() { require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-gateway-stripe-boleto.php'; require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-gateway-stripe-oxxo.php'; require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-stripe-payment-request.php'; + require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-stripe-express-checkout-element.php'; require_once dirname( __FILE__ ) . '/includes/compat/class-wc-stripe-woo-compat-utils.php'; require_once dirname( __FILE__ ) . '/includes/connect/class-wc-stripe-connect.php'; require_once dirname( __FILE__ ) . '/includes/connect/class-wc-stripe-connect-api.php'; @@ -250,10 +258,11 @@ public function init() { require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-account.php'; new Allowed_Payment_Request_Button_Types_Update(); - $this->api = new WC_Stripe_Connect_API(); - $this->connect = new WC_Stripe_Connect( $this->api ); - $this->payment_request_configuration = new WC_Stripe_Payment_Request(); - $this->account = new WC_Stripe_Account( $this->connect, 'WC_Stripe_API' ); + $this->api = new WC_Stripe_Connect_API(); + $this->connect = new WC_Stripe_Connect( $this->api ); + $this->payment_request_configuration = new WC_Stripe_Payment_Request(); + $this->express_checkout_configuration = new WC_Stripe_Express_Checkout_Element(); + $this->account = new WC_Stripe_Account( $this->connect, 'WC_Stripe_API' ); $intent_controller = new WC_Stripe_Intent_Controller(); $intent_controller->init_hooks(); @@ -829,7 +838,7 @@ function woocommerce_gateway_stripe_woocommerce_block_support() { 'woocommerce_blocks_payment_method_type_registration', function( Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry $payment_method_registry ) { // I noticed some incompatibility with WP 5.x and WC 5.3 when `_wcstripe_feature_upe_settings` is enabled. - if ( ! class_exists( 'WC_Stripe_Payment_Request' ) ) { + if ( ! class_exists( 'WC_Stripe_Payment_Request' ) || ! class_exists( 'WC_Stripe_Express_Checkout_Element' ) ) { return; } From ac13e24eb79b87eae82df8fb1e951b39e06a34d8 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Tue, 10 Sep 2024 17:21:13 +0600 Subject: [PATCH 008/120] fix callback function name --- .../class-wc-stripe-express-checkout-element.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-element.php b/includes/payment-methods/class-wc-stripe-express-checkout-element.php index 8e5f1b905..eea4798ec 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-element.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php @@ -86,7 +86,7 @@ public function init() { } add_action( 'template_redirect', [ $this, 'set_session' ] ); - add_action( 'template_redirect', [ $this, 'handle_payment_request_redirect' ] ); + add_action( 'template_redirect', [ $this, 'handle_express_checkout_redirect' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); @@ -589,7 +589,7 @@ public function get_product() { * @return string Redirect URL. */ public function get_login_redirect_url( $redirect ) { - $url = esc_url_raw( wp_unslash( isset( $_COOKIE['wc_stripe_express_checkout_redirect_url'] ) ? $_COOKIE['wc_stripe_payment_request_redirect_url'] : '' ) ); + $url = esc_url_raw( wp_unslash( isset( $_COOKIE['wc_stripe_express_checkout_redirect_url'] ) ? $_COOKIE['wc_stripe_express_checkout_redirect_url'] : '' ) ); if ( empty( $url ) ) { return $redirect; From 0da1b056e9447ec92ca9367bdfb42050ac064ed1 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Tue, 10 Sep 2024 21:33:32 +0600 Subject: [PATCH 009/120] register script for shortcode checkout --- client/entrypoints/express-checkout/index.js | 3 ++ ...ass-wc-stripe-express-checkout-element.php | 28 +++++++++++++++++-- .../class-wc-stripe-payment-request.php | 5 ++++ webpack.config.js | 1 + 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 client/entrypoints/express-checkout/index.js diff --git a/client/entrypoints/express-checkout/index.js b/client/entrypoints/express-checkout/index.js new file mode 100644 index 000000000..b0a327bb3 --- /dev/null +++ b/client/entrypoints/express-checkout/index.js @@ -0,0 +1,3 @@ +// express checkout element integration for shortcode goes here. + +console.log('Express Checkout entrypoint'); diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-element.php b/includes/payment-methods/class-wc-stripe-express-checkout-element.php index eea4798ec..6d9b480e6 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-element.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php @@ -661,10 +661,34 @@ public function scripts() { return; } - $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + $asset_path = WC_STRIPE_PLUGIN_PATH . '/build/express_checkout.asset.php'; + $version = WC_STRIPE_VERSION; + $dependencies = []; + if ( file_exists( $asset_path ) ) { + $asset = require $asset_path; + $version = is_array( $asset ) && isset( $asset['version'] ) + ? $asset['version'] + : $version; + $dependencies = is_array( $asset ) && isset( $asset['dependencies'] ) + ? $asset['dependencies'] + : $dependencies; + } wp_register_script( 'stripe', 'https://js.stripe.com/v3/', '', '3.0', true ); - wp_register_script( 'wc_stripe_express_checkout', plugins_url( 'assets/js/stripe-express_checkout' . $suffix . '.js', WC_STRIPE_MAIN_FILE ), [ 'jquery', 'stripe' ], WC_STRIPE_VERSION, true ); + wp_register_script( + 'wc_stripe_express_checkout', + WC_STRIPE_PLUGIN_URL . '/build/express_checkout.js', + array_merge( [ 'jquery', 'stripe' ], $dependencies ), + $version, + true + ); + + wp_enqueue_style( + 'wc_stripe_express_checkout_style', + WC_STRIPE_PLUGIN_URL . '/build/express_checkout.css', + [], + $version + ); wp_localize_script( 'wc_stripe_express_checkout', diff --git a/includes/payment-methods/class-wc-stripe-payment-request.php b/includes/payment-methods/class-wc-stripe-payment-request.php index 669167fb7..bca2a2801 100644 --- a/includes/payment-methods/class-wc-stripe-payment-request.php +++ b/includes/payment-methods/class-wc-stripe-payment-request.php @@ -84,6 +84,11 @@ public function __construct() { add_action( 'woocommerce_stripe_updated', [ $this, 'migrate_button_size' ] ); + // Check if ECE feature flag is enabled. + if ( WC_Stripe_Feature_Flags::is_stripe_ece_enabled() ) { + return; + } + // Checks if Stripe Gateway is enabled. if ( empty( $this->stripe_settings ) || ( isset( $this->stripe_settings['enabled'] ) && 'yes' !== $this->stripe_settings['enabled'] ) ) { return; diff --git a/webpack.config.js b/webpack.config.js index e7ab20c2f..2dd3ff25e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -63,5 +63,6 @@ module.exports = { upe_blocks: './client/blocks/upe/index.js', upe_settings: './client/settings/index.js', payment_gateways: './client/entrypoints/payment-gateways/index.js', + express_checkout: './client/entrypoints/express-checkout/index.js', }, }; From 92910216dbe772222b8773f734a76077e4114c5a Mon Sep 17 00:00:00 2001 From: Mayisha Date: Tue, 10 Sep 2024 23:32:45 +0600 Subject: [PATCH 010/120] move ajax functions to separate class --- ...c-stripe-express-checkout-ajax-handler.php | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php b/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php new file mode 100644 index 000000000..77cbe34e8 --- /dev/null +++ b/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php @@ -0,0 +1,310 @@ +express_checkout_helper = $express_checkout_helper; + } + + /** + * Initialize hooks. + * + * @return void + */ + public function init() { + add_action( 'wc_ajax_wc_stripe_get_cart_details', [ $this, 'ajax_get_cart_details' ] ); + add_action( 'wc_ajax_wc_stripe_get_shipping_options', [ $this, 'ajax_get_shipping_options' ] ); + add_action( 'wc_ajax_wc_stripe_update_shipping_method', [ $this, 'ajax_update_shipping_method' ] ); + add_action( 'wc_ajax_wc_stripe_create_order', [ $this, 'ajax_create_order' ] ); + add_action( 'wc_ajax_wc_stripe_add_to_cart', [ $this, 'ajax_add_to_cart' ] ); + add_action( 'wc_ajax_wc_stripe_get_selected_product_data', [ $this, 'ajax_get_selected_product_data' ] ); + add_action( 'wc_ajax_wc_stripe_clear_cart', [ $this, 'ajax_clear_cart' ] ); + add_action( 'wc_ajax_wc_stripe_log_errors', [ $this, 'ajax_log_errors' ] ); + } + + /** + * Get cart details. + */ + public function ajax_get_cart_details() { + check_ajax_referer( 'wc-stripe-express-checkout', 'security' ); + + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + WC()->cart->calculate_totals(); + + $currency = get_woocommerce_currency(); + + // Set mandatory payment details. + $data = [ + 'shipping_required' => WC()->cart->needs_shipping(), + 'order_data' => [ + 'currency' => strtolower( $currency ), + 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ), + ], + ]; + + $data['order_data'] += $this->express_checkout_helper->build_display_items(); + + wp_send_json( $data ); + } + + + /** + * Adds the current product to the cart. Used on product detail page. + * + * @return array $data Results of adding the product to the cart. + */ + public function ajax_add_to_cart() { + check_ajax_referer( 'wc-stripe-add-to-cart', 'security' ); + + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + WC()->shipping->reset_shipping(); + + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; + $qty = ! isset( $_POST['qty'] ) ? 1 : absint( $_POST['qty'] ); + $product = wc_get_product( $product_id ); + $product_type = $product->get_type(); + + // First empty the cart to prevent wrong calculation. + WC()->cart->empty_cart(); + + if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) { + $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); + + $data_store = WC_Data_Store::load( 'product' ); + $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); + + WC()->cart->add_to_cart( $product->get_id(), $qty, $variation_id, $attributes ); + } + + if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation' ], true ) ) { + WC()->cart->add_to_cart( $product->get_id(), $qty ); + } + + WC()->cart->calculate_totals(); + + $data = []; + $data += $this->express_checkout_helper->build_display_items(); + $data['result'] = 'success'; + + // @phpstan-ignore-next-line (return statement is added) + wp_send_json( $data ); + } + + /** + * Clears cart. + */ + public function ajax_clear_cart() { + check_ajax_referer( 'wc-stripe-clear-cart', 'security' ); + + WC()->cart->empty_cart(); + exit; + } + + /** + * Get shipping options. + * + * @see WC_Cart::get_shipping_packages(). + * @see WC_Shipping::calculate_shipping(). + * @see WC_Shipping::get_packages(). + */ + public function ajax_get_shipping_options() { + check_ajax_referer( 'wc-stripe-express-checkout-shipping', 'security' ); + + $shipping_address = filter_input_array( + INPUT_POST, + [ + 'country' => FILTER_SANITIZE_SPECIAL_CHARS, + 'state' => FILTER_SANITIZE_SPECIAL_CHARS, + 'postcode' => FILTER_SANITIZE_SPECIAL_CHARS, + 'city' => FILTER_SANITIZE_SPECIAL_CHARS, + 'address' => FILTER_SANITIZE_SPECIAL_CHARS, + 'address_2' => FILTER_SANITIZE_SPECIAL_CHARS, + ] + ); + $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] ); + $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); + + $data = $this->express_checkout_helper->get_shipping_options( $shipping_address, $should_show_itemized_view ); + wp_send_json( $data ); + } + + /** + * Update shipping method. + */ + public function ajax_update_shipping_method() { + check_ajax_referer( 'wc-stripe-update-shipping-method', 'security' ); + + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + $shipping_methods = filter_input( INPUT_POST, 'shipping_method', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); + $this->express_checkout_helper->update_shipping_method( $shipping_methods ); + + WC()->cart->calculate_totals(); + + $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] ); + $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); + + $data = []; + $data += $this->express_checkout_helper->build_display_items( $should_show_itemized_view ); + $data['result'] = 'success'; + + wp_send_json( $data ); + } + + /** + * Gets the selected product data. + * + * @return array $data The selected product data. + */ + public function ajax_get_selected_product_data() { + check_ajax_referer( 'wc-stripe-get-selected-product-data', 'security' ); + + try { // @phpstan-ignore-line (return statement is added) + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; + $qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id ); + $addon_value = isset( $_POST['addon_value'] ) ? max( floatval( $_POST['addon_value'] ), 0 ) : 0; + $product = wc_get_product( $product_id ); + $variation_id = null; + + if ( ! is_a( $product, 'WC_Product' ) ) { + /* translators: 1) The product Id */ + throw new Exception( sprintf( __( 'Product with the ID (%1$s) cannot be found.', 'woocommerce-gateway-stripe' ), $product_id ) ); + } + + if ( in_array( $product->get_type(), [ 'variable', 'variable-subscription' ], true ) && isset( $_POST['attributes'] ) ) { + $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); + + $data_store = WC_Data_Store::load( 'product' ); + $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); + + if ( ! empty( $variation_id ) ) { + $product = wc_get_product( $variation_id ); + } + } + + if ( $this->express_checkout_helper->is_invalid_subscription_product( $product, true ) ) { + throw new Exception( __( 'The chosen subscription product is not supported.', 'woocommerce-gateway-stripe' ) ); + } + + // Force quantity to 1 if sold individually and check for existing item in cart. + if ( $product->is_sold_individually() ) { + $qty = apply_filters( 'wc_stripe_payment_request_add_to_cart_sold_individually_quantity', 1, $qty, $product_id, $variation_id ); + } + + if ( ! $product->has_enough_stock( $qty ) ) { + /* translators: 1) product name 2) quantity in stock */ + throw new Exception( sprintf( __( 'You cannot add that amount of "%1$s"; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce-gateway-stripe' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) ); + } + + $total = $qty * $this->express_checkout_helper->get_product_price( $product ) + $addon_value; + + $quantity_label = 1 < $qty ? ' (x' . $qty . ')' : ''; + + $data = []; + $items = []; + + $items[] = [ + 'label' => $product->get_name() . $quantity_label, + 'amount' => WC_Stripe_Helper::get_stripe_amount( $total ), + ]; + + if ( wc_tax_enabled() ) { + $items[] = [ + 'label' => __( 'Tax', 'woocommerce-gateway-stripe' ), + 'amount' => 0, + 'pending' => true, + ]; + } + + if ( wc_shipping_enabled() && $product->needs_shipping() ) { + $items[] = [ + 'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ), + 'amount' => 0, + 'pending' => true, + ]; + + $data['shippingOptions'] = [ + 'id' => 'pending', + 'label' => __( 'Pending', 'woocommerce-gateway-stripe' ), + 'detail' => '', + 'amount' => 0, + ]; + } + + $data['displayItems'] = $items; + $data['total'] = [ + 'label' => $this->express_checkout_helper->get_total_label(), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $total ), + ]; + + $data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() ); + $data['currency'] = strtolower( get_woocommerce_currency() ); + $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); + + wp_send_json( $data ); + } catch ( Exception $e ) { + wp_send_json( [ 'error' => wp_strip_all_tags( $e->getMessage() ) ] ); + } + } + + /** + * Create order. Security is handled by WC. + */ + public function ajax_create_order() { + if ( WC()->cart->is_empty() ) { + wp_send_json_error( __( 'Empty cart', 'woocommerce-gateway-stripe' ) ); + } + + if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { + define( 'WOOCOMMERCE_CHECKOUT', true ); + } + + // Normalizes billing and shipping state values. + $this->express_checkout_helper->normalize_state(); + + // In case the state is required, but is missing, add a more descriptive error notice. + $this->express_checkout_helper->validate_state(); + + WC()->checkout()->process_checkout(); + + die( 0 ); + } + + /** + * Log errors coming from express checkout elements + */ + public function ajax_log_errors() { + check_ajax_referer( 'wc-stripe-log-errors', 'security' ); + + $errors = isset( $_POST['errors'] ) ? wc_clean( wp_unslash( $_POST['errors'] ) ) : ''; + + WC_Stripe_Logger::log( $errors ); + + exit; + } +} From 8c6f05d89d2ec47e73dccaea4910ff711242fa1c Mon Sep 17 00:00:00 2001 From: Mayisha Date: Wed, 11 Sep 2024 00:29:38 +0600 Subject: [PATCH 011/120] move helper functions to a separate class --- ...ass-wc-stripe-express-checkout-element.php | 1583 +---------------- ...lass-wc-stripe-express-checkout-helper.php | 1185 ++++++++++++ 2 files changed, 1268 insertions(+), 1500 deletions(-) create mode 100644 includes/payment-methods/class-wc-stripe-express-checkout-helper.php diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-element.php b/includes/payment-methods/class-wc-stripe-express-checkout-element.php index 6d9b480e6..7e41cf8cf 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-element.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php @@ -1,10 +1,4 @@ stripe_settings = WC_Stripe_Helper::get_stripe_settings(); - $this->testmode = ( ! empty( $this->stripe_settings['testmode'] ) && 'yes' === $this->stripe_settings['testmode'] ) ? true : false; - $this->total_label = ! empty( $this->stripe_settings['statement_descriptor'] ) ? WC_Stripe_Helper::clean_statement_descriptor( $this->stripe_settings['statement_descriptor'] ) : ''; - - $this->total_label = str_replace( "'", '', $this->total_label ) . apply_filters( 'wc_stripe_payment_request_total_label_suffix', ' (via WooCommerce)' ); - $this->init(); + $this->express_checkout_helper = $express_checkout_helper; + $this->express_checkout_ajax_handler = $express_checkout_ajax_handler; + $this->express_checkout_ajax_handler->init(); } /** @@ -76,7 +65,7 @@ public function init() { } // Don't initiate this class if express checkout element is disabled. - if ( ! $this->is_express_checkout_enabled() ) { + if ( ! $this->express_checkout_helper->is_express_checkout_enabled() ) { return; } @@ -94,15 +83,6 @@ public function init() { add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_express_checkout_button_html' ], 25 ); add_action( 'woocommerce_checkout_before_customer_details', [ $this, 'display_express_checkout_button_html' ], 1 ); - add_action( 'wc_ajax_wc_stripe_get_cart_details', [ $this, 'ajax_get_cart_details' ] ); - add_action( 'wc_ajax_wc_stripe_get_shipping_options', [ $this, 'ajax_get_shipping_options' ] ); - add_action( 'wc_ajax_wc_stripe_update_shipping_method', [ $this, 'ajax_update_shipping_method' ] ); - add_action( 'wc_ajax_wc_stripe_create_order', [ $this, 'ajax_create_order' ] ); - add_action( 'wc_ajax_wc_stripe_add_to_cart', [ $this, 'ajax_add_to_cart' ] ); - add_action( 'wc_ajax_wc_stripe_get_selected_product_data', [ $this, 'ajax_get_selected_product_data' ] ); - add_action( 'wc_ajax_wc_stripe_clear_cart', [ $this, 'ajax_clear_cart' ] ); - add_action( 'wc_ajax_wc_stripe_log_errors', [ $this, 'ajax_log_errors' ] ); - add_filter( 'woocommerce_gateway_title', [ $this, 'filter_gateway_title' ], 10, 2 ); add_action( 'woocommerce_checkout_order_processed', [ $this, 'add_order_meta' ], 10, 2 ); add_filter( 'woocommerce_login_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); @@ -118,39 +98,6 @@ public static function instance() { return self::$_this; } - /** - * Checks whether authentication is required for checkout. - * - * @return bool - */ - public function is_authentication_required() { - // If guest checkout is disabled and account creation upon checkout is not possible, authentication is required. - if ( 'no' === get_option( 'woocommerce_enable_guest_checkout', 'yes' ) && ! $this->is_account_creation_possible() ) { - return true; - } - // If cart contains subscription and account creation upon checkout is not posible, authentication is required. - if ( $this->has_subscription_product() && ! $this->is_account_creation_possible() ) { - return true; - } - - return false; - } - - /** - * Checks whether account creation is possible upon checkout. - * - * @return bool - */ - public function is_account_creation_possible() { - // If automatically generate username/password are disabled, we can not include any of those fields, - // during express checkout. So account creation is not possible. - return ( - 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'no' ) && - 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && - 'yes' === get_option( 'woocommerce_registration_generate_password', 'yes' ) - ); - } - /** * Sets the WC customer session if one is not set. * This is needed so nonces can be verified by AJAX Request. @@ -158,7 +105,7 @@ public function is_account_creation_possible() { * @return void */ public function set_session() { - if ( ! $this->is_product() || ( isset( WC()->session ) && WC()->session->has_session() ) ) { + if ( ! $this->express_checkout_helper->is_product() || ( isset( WC()->session ) && WC()->session->has_session() ) ) { return; } @@ -184,404 +131,6 @@ public function handle_express_checkout_redirect() { } } - /** - * Gets the button type. - * - * @return string - */ - public function get_button_type() { - return isset( $this->stripe_settings['payment_request_button_type'] ) ? $this->stripe_settings['payment_request_button_type'] : 'default'; - } - - /** - * Gets the button theme. - * - * @return string - */ - public function get_button_theme() { - return isset( $this->stripe_settings['payment_request_button_theme'] ) ? $this->stripe_settings['payment_request_button_theme'] : 'dark'; - } - - /** - * Gets the button height. - * - * @return string - */ - public function get_button_height() { - $height = isset( $this->stripe_settings['payment_request_button_size'] ) ? $this->stripe_settings['payment_request_button_size'] : 'default'; - if ( 'small' === $height ) { - return '40'; - } - - if ( 'large' === $height ) { - return '56'; - } - - return '48'; - } - - /** - * Gets the product total price. - * - * @param object $product WC_Product_* object. - * @return integer Total price. - */ - public function get_product_price( $product ) { - $product_price = $product->get_price(); - // Add subscription sign-up fees to product price. - if ( in_array( $product->get_type(), [ 'subscription', 'subscription_variation' ] ) && class_exists( 'WC_Subscriptions_Product' ) ) { - $product_price = $product->get_price() + WC_Subscriptions_Product::get_sign_up_fee( $product ); - } - - return $product_price; - } - - /** - * Gets the product data for the currently viewed page - * - * @return mixed Returns false if not on a product page, the product information otherwise. - */ - public function get_product_data() { - if ( ! $this->is_product() ) { - return false; - } - - $product = $this->get_product(); - $variation_id = 0; - - if ( in_array( $product->get_type(), [ 'variable', 'variable-subscription' ], true ) ) { - $variation_attributes = $product->get_variation_attributes(); - $attributes = []; - - foreach ( $variation_attributes as $attribute_name => $attribute_values ) { - $attribute_key = 'attribute_' . sanitize_title( $attribute_name ); - - // Passed value via GET takes precedence, then POST, otherwise get the default value for given attribute - if ( isset( $_GET[ $attribute_key ] ) ) { - $attributes[ $attribute_key ] = wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ); - } elseif ( isset( $_POST[ $attribute_key ] ) ) { - $attributes[ $attribute_key ] = wc_clean( wp_unslash( $_POST[ $attribute_key ] ) ); - } else { - $attributes[ $attribute_key ] = $product->get_variation_default_attribute( $attribute_name ); - } - } - - $data_store = WC_Data_Store::load( 'product' ); - $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); - - if ( ! empty( $variation_id ) ) { - $product = wc_get_product( $variation_id ); - } - } - - $data = []; - $items = []; - - $items[] = [ - 'label' => $product->get_name(), - 'amount' => WC_Stripe_Helper::get_stripe_amount( $this->get_product_price( $product ) ), - ]; - - if ( wc_tax_enabled() ) { - $items[] = [ - 'label' => __( 'Tax', 'woocommerce-gateway-stripe' ), - 'amount' => 0, - 'pending' => true, - ]; - } - - if ( wc_shipping_enabled() && $product->needs_shipping() ) { - $items[] = [ - 'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ), - 'amount' => 0, - 'pending' => true, - ]; - - $data['shippingOptions'] = [ - 'id' => 'pending', - 'label' => __( 'Pending', 'woocommerce-gateway-stripe' ), - 'detail' => '', - 'amount' => 0, - ]; - } - - $data['displayItems'] = $items; - $data['total'] = [ - 'label' => apply_filters( 'wc_stripe_payment_request_total_label', $this->total_label ), - 'amount' => WC_Stripe_Helper::get_stripe_amount( $this->get_product_price( $product ) ), - ]; - - $data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() && 0 !== wc_get_shipping_method_count( true ) ); - $data['currency'] = strtolower( get_woocommerce_currency() ); - $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); - - // On product page load, if there's a variation already selected, check if it's supported. - $data['validVariationSelected'] = ! empty( $variation_id ) ? $this->is_product_supported( $product ) : true; - - return apply_filters( 'wc_stripe_payment_request_product_data', $data, $product ); - } - - /** - * Filters the gateway title to reflect express checkout type - */ - public function filter_gateway_title( $title, $id ) { - global $theorder; - - // If $theorder is empty (i.e. non-HPOS), fallback to using the global post object. - if ( empty( $theorder ) && ! empty( $GLOBALS['post']->ID ) ) { - $theorder = wc_get_order( $GLOBALS['post']->ID ); - } - - if ( ! is_object( $theorder ) ) { - return $title; - } - - $method_title = $theorder->get_payment_method_title(); - - if ( 'stripe' === $id && ! empty( $method_title ) ) { - if ( 'Apple Pay (Stripe)' === $method_title - || 'Google Pay (Stripe)' === $method_title - ) { - return $method_title; - } - } - - return $title; - } - - /** - * Normalizes postal code in case of redacted data from Apple Pay. - * - * @param string $postcode Postal code. - * @param string $country Country. - */ - public function get_normalized_postal_code( $postcode, $country ) { - /** - * Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively - * when passing it back from the shippingcontactselected object. This causes WC to invalidate - * the postal code and not calculate shipping zones correctly. - */ - if ( 'GB' === $country ) { - // Replaces a redacted string with something like LN10***. - return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '*' ); - } - if ( 'CA' === $country ) { - // Replaces a redacted string with something like L4Y***. - return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '*' ); - } - - return $postcode; - } - - /** - * Add needed order meta - * - * @param integer $order_id The order ID. - * @param array $posted_data The posted data from checkout form. - * - * @return void - */ - public function add_order_meta( $order_id, $posted_data ) { - if ( empty( $_POST['express_checkout_type'] ) || ! isset( $_POST['payment_method'] ) || 'stripe' !== $_POST['payment_method'] ) { - return; - } - - $order = wc_get_order( $order_id ); - - $express_checkout_type = wc_clean( wp_unslash( $_POST['express_checkout_type'] ) ); - - if ( 'apple_pay' === $express_checkout_type ) { - $order->set_payment_method_title( 'Apple Pay (Stripe)' ); - $order->save(); - } elseif ( 'google_pay' === $express_checkout_type ) { - $order->set_payment_method_title( 'Google Pay (Stripe)' ); - $order->save(); - } - } - - /** - * Checks to make sure product type is supported. - * - * @return array - */ - public function supported_product_types() { - return apply_filters( - 'wc_stripe_payment_request_supported_types', - [ - 'simple', - 'variable', - 'variation', - 'subscription', - 'variable-subscription', - 'subscription_variation', - 'booking', - 'bundle', - 'composite', - ] - ); - } - - /** - * Checks the cart to see if all items are allowed to be used. - * - * @return boolean - */ - public function allowed_items_in_cart() { - // Pre Orders compatibility where we don't support charge upon release. - if ( $this->is_pre_order_item_in_cart() && $this->is_pre_order_product_charged_upon_release( $this->get_pre_order_product_from_cart() ) ) { - return false; - } - - // If the cart is not available we don't have any unsupported products in the cart, so we - // return true. This can happen e.g. when loading the cart or checkout blocks in Gutenberg. - if ( is_null( WC()->cart ) ) { - return true; - } - - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); - - if ( ! in_array( $_product->get_type(), $this->supported_product_types() ) ) { - return false; - } - - // Subscriptions with a trial period that need shipping are not supported. - if ( $this->is_invalid_subscription_product( $_product ) ) { - return false; - } - } - - // We don't support multiple packages with express checkout buttons because we can't offer - // a good UX. - $packages = WC()->cart->get_shipping_packages(); - if ( 1 < count( $packages ) ) { - return false; - } - - return true; - } - - /** - * Returns true if the given product is a subscription that cannot be purchased with express checkout buttons. - * - * Invalid subscription products include those with: - * - a free trial that requires shipping (synchronised subscriptions with a delayed first payment are considered to have a free trial) - * - a synchronised subscription with no upfront payment and is virtual (this limitation only applies to the product page as we cannot calculate totals correctly) - * - * If the product is a variable subscription, this function will return true if all of its variations have a trial and require shipping. - * - * @since 7.8.0 - * - * @param WC_Product|null $product Product object. - * @param boolean $is_product_page_request Whether this is a request from the product page. - * - * @return boolean - */ - public function is_invalid_subscription_product( $product, $is_product_page_request = false ) { - if ( ! class_exists( 'WC_Subscriptions_Product' ) || ! class_exists( 'WC_Subscriptions_Synchroniser' ) || ! WC_Subscriptions_Product::is_subscription( $product ) ) { - return false; - } - - $is_invalid = true; - - if ( $product->get_type() === 'variable-subscription' ) { - $products = $product->get_available_variations( 'object' ); - } else { - $products = [ $product ]; - } - - foreach ( $products as $product ) { - $needs_shipping = $product->needs_shipping(); - $is_synced = WC_Subscriptions_Synchroniser::is_product_synced( $product ); - $is_payment_upfront = WC_Subscriptions_Synchroniser::is_payment_upfront( $product ); - $has_trial_period = WC_Subscriptions_Product::get_trial_length( $product ) > 0; - - if ( $is_product_page_request && $is_synced && ! $is_payment_upfront && ! $needs_shipping ) { - /** - * This condition prevents the purchase of virtual synced subscription products with no upfront costs via express checkout buttons from the product page. - * - * The main issue is that calling $product->get_price() on a synced subscription does not take into account a mock trial period or prorated price calculations - * until the product is in the cart. This means that the totals passed to express checkout element are incorrect when purchasing from the product page. - * Another part of the problem is because the product is virtual this stops the Stripe PaymentRequest API from triggering the necessary `shippingaddresschange` event - * which is when we call WC()->cart->calculate_totals(); which would fix the totals. - * - * The fix here is to not allow virtual synced subscription products with no upfront costs to be purchased via express checkout buttons on the product page. - */ - continue; - } elseif ( $is_synced && ! $is_payment_upfront && $needs_shipping ) { - continue; - } elseif ( $has_trial_period && $needs_shipping ) { - continue; - } else { - // If we made it this far, the product is valid. Break out of the foreach and return early as we only care about invalid cases. - $is_invalid = false; - break; - } - } - - return $is_invalid; - } - - /** - * Checks whether cart contains a subscription product or this is a subscription product page. - * - * @return boolean - */ - public function has_subscription_product() { - if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { - return false; - } - - if ( $this->is_product() ) { - $product = $this->get_product(); - if ( WC_Subscriptions_Product::is_subscription( $product ) ) { - return true; - } - } elseif ( WC_Stripe_Helper::has_cart_or_checkout_on_current_page() ) { - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); - if ( WC_Subscriptions_Product::is_subscription( $_product ) ) { - return true; - } - } - } - - return false; - } - - /** - * Checks if this is a product page or content contains a product_page shortcode. - * - * @return boolean - */ - public function is_product() { - return is_product() || wc_post_content_has_shortcode( 'product_page' ); - } - - /** - * Get product from product page or product_page shortcode. - * - * @return WC_Product Product object. - */ - public function get_product() { - global $post; - - if ( is_product() ) { - return wc_get_product( $post->ID ); - } elseif ( wc_post_content_has_shortcode( 'product_page' ) ) { - // Get id from product_page shortcode. - preg_match( '/\[product_page id="(?\d+)"\]/', $post->post_content, $shortcode_match ); - - if ( ! isset( $shortcode_match['id'] ) ) { - return false; - } - - return wc_get_product( $shortcode_match['id'] ); - } - - return false; - } - /** * Returns the login redirect URL. * @@ -616,7 +165,7 @@ public function javascript_params() { 'allow_prepaid_card' => apply_filters( 'wc_stripe_allow_prepaid_card', true ) ? 'yes' : 'no', 'locale' => WC_Stripe_Helper::convert_wc_locale_to_stripe_locale( get_locale() ), 'is_link_enabled' => WC_Stripe_UPE_Payment_Method_Link::is_link_enabled(), - 'is_express_checkout_enabled' => $this->is_express_checkout_enabled(), + 'is_express_checkout_enabled' => $this->express_checkout_helper->is_express_checkout_enabled(), ], 'nonce' => [ 'payment' => wp_create_nonce( 'wc-stripe-express-checkout-element' ), @@ -641,10 +190,10 @@ public function javascript_params() { // Defaults to 'required' to match how core initializes this option. 'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), ], - 'button' => $this->get_button_settings(), - 'login_confirmation' => $this->get_login_confirmation_settings(), - 'is_product_page' => $this->is_product(), - 'product' => $this->get_product_data(), + 'button' => $this->express_checkout_helper->get_button_settings(), + 'login_confirmation' => $this->express_checkout_helper->get_login_confirmation_settings(), + 'is_product_page' => $this->express_checkout_helper->is_product(), + 'product' => $this->express_checkout_helper->get_product_data(), ]; } @@ -653,11 +202,11 @@ public function javascript_params() { */ public function scripts() { // If page is not supported, bail. - if ( ! $this->is_page_supported() ) { + if ( ! $this->express_checkout_helper->is_page_supported() ) { return; } - if ( ! $this->should_show_express_checkout_button() ) { + if ( ! $this->express_checkout_helper->should_show_express_checkout_button() ) { return; } @@ -703,1067 +252,101 @@ public function scripts() { } /** - * Returns true if the current page supports Express Checkout Buttons, false otherwise. + * Add needed order meta + * + * @param integer $order_id The order ID. + * @param array $posted_data The posted data from checkout form. * - * @return boolean True if the current page is supported, false otherwise. + * @return void */ - private function is_page_supported() { - return $this->is_product() - || WC_Stripe_Helper::has_cart_or_checkout_on_current_page() - || is_wc_endpoint_url( 'order-pay' ); + public function add_order_meta( $order_id, $posted_data ) { + if ( empty( $_POST['express_checkout_type'] ) || ! isset( $_POST['payment_method'] ) || 'stripe' !== $_POST['payment_method'] ) { + return; + } + + $order = wc_get_order( $order_id ); + + $express_checkout_type = wc_clean( wp_unslash( $_POST['express_checkout_type'] ) ); + + if ( 'apple_pay' === $express_checkout_type ) { + $order->set_payment_method_title( 'Apple Pay (Stripe)' ); + $order->save(); + } elseif ( 'google_pay' === $express_checkout_type ) { + $order->set_payment_method_title( 'Google Pay (Stripe)' ); + $order->save(); + } } /** - * Display the express checkout button. + * Filters the gateway title to reflect express checkout type */ - public function display_express_checkout_button_html() { - $gateways = WC()->payment_gateways->get_available_payment_gateways(); + public function filter_gateway_title( $title, $id ) { + global $theorder; - if ( ! isset( $gateways['stripe'] ) ) { - return; + // If $theorder is empty (i.e. non-HPOS), fallback to using the global post object. + if ( empty( $theorder ) && ! empty( $GLOBALS['post']->ID ) ) { + $theorder = wc_get_order( $GLOBALS['post']->ID ); } - if ( ! $this->is_page_supported() ) { - return; + if ( ! is_object( $theorder ) ) { + return $title; } - if ( ! $this->should_show_express_checkout_button() ) { - return; + $method_title = $theorder->get_payment_method_title(); + + if ( 'stripe' === $id && ! empty( $method_title ) ) { + if ( 'Apple Pay (Stripe)' === $method_title + || 'Google Pay (Stripe)' === $method_title + ) { + return $method_title; + } } - ?> - - display_express_checkout_button_separator_html(); + return $title; } /** - * Display express checkout button separator. + * Display the express checkout button. */ - public function display_express_checkout_button_separator_html() { - if ( ! is_checkout() && ! is_wc_endpoint_url( 'order-pay' ) ) { - return; - } + public function display_express_checkout_button_html() { + $gateways = WC()->payment_gateways->get_available_payment_gateways(); - if ( is_checkout() && ! in_array( 'checkout', $this->get_button_locations(), true ) ) { + if ( ! isset( $gateways['stripe'] ) ) { return; } - ?> - - connect->is_connected() ) { - WC_Stripe_Logger::log( 'Account is not connected.' ); - return false; - } - - // If no SSL bail. - if ( ! $this->testmode && ! is_ssl() ) { - WC_Stripe_Logger::log( 'Stripe Express Checkout live mode requires SSL.' ); - return false; - } - - // Don't show if on the cart or checkout page, or if page contains the cart or checkout - // shortcodes, with items in the cart that aren't supported. - if ( - WC_Stripe_Helper::has_cart_or_checkout_on_current_page() - && ! $this->allowed_items_in_cart() - ) { - return false; - } - - // Don't show on cart if disabled. - if ( is_cart() && ! $this->should_show_ece_on_cart_page() ) { - return false; - } - - // Don't show on checkout if disabled. - if ( is_checkout() && ! $this->should_show_ece_on_checkout_page() ) { - return false; - } - - // Don't show if product page PRB is disabled. - if ( $this->is_product() && ! $this->should_show_ece_on_product_pages() ) { - return false; - } - - // Don't show if product on current page is not supported. - if ( $this->is_product() && ! $this->is_product_supported( $this->get_product() ) ) { - return false; - } - - if ( $this->is_product() && in_array( $this->get_product()->get_type(), [ 'variable', 'variable-subscription' ], true ) ) { - $stock_availability = array_column( $this->get_product()->get_available_variations(), 'is_in_stock' ); - // Don't show if all product variations are out-of-stock. - if ( ! in_array( true, $stock_availability, true ) ) { - return false; - } - } - - return true; - } - - /** - * Returns true if express checkout buttons are enabled on the cart page, false - * otherwise. - * - * @return boolean True if express checkout buttons are enabled on the cart page, false otherwise - */ - public function should_show_ece_on_cart_page() { - $should_show_on_cart_page = in_array( 'cart', $this->get_button_locations(), true ); - - return apply_filters( - 'wc_stripe_show_payment_request_on_cart', - $should_show_on_cart_page - ); - } - - /** - * Returns true if express checkout buttons are enabled on the checkout page, false - * otherwise. - * - * @return boolean True if express checkout buttons are enabled on the checkout page, false otherwise - */ - public function should_show_ece_on_checkout_page() { - global $post; - - $should_show_on_checkout_page = in_array( 'checkout', $this->get_button_locations(), true ); - - return apply_filters( - 'wc_stripe_show_payment_request_on_checkout', - $should_show_on_checkout_page, - $post - ); - } - - /** - * Returns true if express checkout buttons are enabled on product pages, false - * otherwise. - * - * @return boolean True if express checkout buttons are enabled on product pages, false otherwise - */ - public function should_show_ece_on_product_pages() { - global $post; - - $should_show_on_product_page = in_array( 'product', $this->get_button_locations(), true ); - - // Note the negation because if the filter returns `true` that means we should hide the PRB. - return ! apply_filters( - 'wc_stripe_hide_payment_request_on_product_page', - ! $should_show_on_product_page, - $post - ); - } - - /** - * Returns true if a the provided product is supported, false otherwise. - * - * @param WC_Product $param The product that's being checked for support. - * - * @return boolean True if the provided product is supported, false otherwise. - */ - private function is_product_supported( $product ) { - if ( ! is_object( $product ) || ! in_array( $product->get_type(), $this->supported_product_types() ) ) { - return false; - } - - // Trial subscriptions with shipping are not supported. - if ( $this->is_invalid_subscription_product( $product, true ) ) { - return false; - } - - // Pre Orders charge upon release not supported. - if ( $this->is_pre_order_product_charged_upon_release( $product ) ) { - return false; - } - - // Composite products are not supported on the product page. - if ( class_exists( 'WC_Composite_Products' ) && function_exists( 'is_composite_product' ) && is_composite_product() ) { - return false; - } - - // File upload addon not supported - if ( class_exists( 'WC_Product_Addons_Helper' ) ) { - $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() ); - foreach ( $product_addons as $addon ) { - if ( 'file_upload' === $addon['type'] ) { - return false; - } - } - } - - return true; - } - - /** - * Log errors coming from express checkout elements - */ - public function ajax_log_errors() { - check_ajax_referer( 'wc-stripe-log-errors', 'security' ); - - $errors = isset( $_POST['errors'] ) ? wc_clean( wp_unslash( $_POST['errors'] ) ) : ''; - - WC_Stripe_Logger::log( $errors ); - - exit; - } - - /** - * Clears cart. - */ - public function ajax_clear_cart() { - check_ajax_referer( 'wc-stripe-clear-cart', 'security' ); - - WC()->cart->empty_cart(); - exit; - } - - /** - * Get cart details. - */ - public function ajax_get_cart_details() { - check_ajax_referer( 'wc-stripe-express-checkout', 'security' ); - - if ( ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - } - - WC()->cart->calculate_totals(); - $currency = get_woocommerce_currency(); - - // Set mandatory payment details. - $data = [ - 'shipping_required' => WC()->cart->needs_shipping(), - 'order_data' => [ - 'currency' => strtolower( $currency ), - 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ), - ], - ]; - - $data['order_data'] += $this->build_display_items(); - - wp_send_json( $data ); - } - - /** - * Get shipping options. - * - * @see WC_Cart::get_shipping_packages(). - * @see WC_Shipping::calculate_shipping(). - * @see WC_Shipping::get_packages(). - */ - public function ajax_get_shipping_options() { - check_ajax_referer( 'wc-stripe-express-checkout-shipping', 'security' ); - - $shipping_address = filter_input_array( - INPUT_POST, - [ - 'country' => FILTER_SANITIZE_SPECIAL_CHARS, - 'state' => FILTER_SANITIZE_SPECIAL_CHARS, - 'postcode' => FILTER_SANITIZE_SPECIAL_CHARS, - 'city' => FILTER_SANITIZE_SPECIAL_CHARS, - 'address' => FILTER_SANITIZE_SPECIAL_CHARS, - 'address_2' => FILTER_SANITIZE_SPECIAL_CHARS, - ] - ); - $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] ); - $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); - - $data = $this->get_shipping_options( $shipping_address, $should_show_itemized_view ); - wp_send_json( $data ); - } - - /** - * Gets shipping options available for specified shipping address - * - * @param array $shipping_address Shipping address. - * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views. - * - * @return array Shipping options data. - * - * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag - */ - public function get_shipping_options( $shipping_address, $itemized_display_items = false ) { - try { - // Set the shipping options. - $data = []; - - // Remember current shipping method before resetting. - $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); - $this->calculate_shipping( apply_filters( 'wc_stripe_payment_request_shipping_posted_values', $shipping_address ) ); - - $packages = WC()->shipping->get_packages(); - $shipping_rate_ids = []; - - if ( ! empty( $packages ) && WC()->customer->has_calculated_shipping() ) { - foreach ( $packages as $package_key => $package ) { - if ( empty( $package['rates'] ) ) { - throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-gateway-stripe' ) ); - } - - foreach ( $package['rates'] as $key => $rate ) { - if ( in_array( $rate->id, $shipping_rate_ids, true ) ) { - // The express checkout will try to load indefinitely if there are duplicate shipping - // option IDs. - throw new Exception( __( 'Unable to provide shipping options for express checkout.', 'woocommerce-gateway-stripe' ) ); - } - $shipping_rate_ids[] = $rate->id; - $data['shipping_options'][] = [ - 'id' => $rate->id, - 'label' => $rate->label, - 'detail' => '', - 'amount' => WC_Stripe_Helper::get_stripe_amount( $rate->cost ), - ]; - } - } - } else { - throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-gateway-stripe' ) ); - } - - // The first shipping option is automatically applied on the client. - // Keep chosen shipping method by sorting shipping options if the method still available for new address. - // Fallback to the first available shipping method. - if ( isset( $data['shipping_options'][0] ) ) { - if ( isset( $chosen_shipping_methods[0] ) ) { - $chosen_method_id = $chosen_shipping_methods[0]; - $compare_shipping_options = function ( $a, $b ) use ( $chosen_method_id ) { - if ( $a['id'] === $chosen_method_id ) { - return -1; - } - - if ( $b['id'] === $chosen_method_id ) { - return 1; - } - - return 0; - }; - usort( $data['shipping_options'], $compare_shipping_options ); - } - - $first_shipping_method_id = $data['shipping_options'][0]['id']; - $this->update_shipping_method( [ $first_shipping_method_id ] ); - } - - WC()->cart->calculate_totals(); - - $this->maybe_restore_recurring_chosen_shipping_methods( $chosen_shipping_methods ); - - $data += $this->build_display_items( $itemized_display_items ); - $data['result'] = 'success'; - } catch ( Exception $e ) { - $data += $this->build_display_items( $itemized_display_items ); - $data['result'] = 'invalid_shipping_address'; - } - - return $data; - } - - /** - * Update shipping method. - */ - public function ajax_update_shipping_method() { - check_ajax_referer( 'wc-stripe-update-shipping-method', 'security' ); - - if ( ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - } - - $shipping_methods = filter_input( INPUT_POST, 'shipping_method', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); - $this->update_shipping_method( $shipping_methods ); - - WC()->cart->calculate_totals(); - - $product_view_options = filter_input_array( INPUT_POST, [ 'is_product_page' => FILTER_SANITIZE_SPECIAL_CHARS ] ); - $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); - - $data = []; - $data += $this->build_display_items( $should_show_itemized_view ); - $data['result'] = 'success'; - - wp_send_json( $data ); - } - - /** - * Updates shipping method in WC session - * - * @param array $shipping_methods Array of selected shipping methods ids. - */ - public function update_shipping_method( $shipping_methods ) { - $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); - - if ( is_array( $shipping_methods ) ) { - foreach ( $shipping_methods as $i => $value ) { - $chosen_shipping_methods[ $i ] = wc_clean( $value ); - } - } - - WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); - } - - /** - * Gets the selected product data. - * - * @return array $data The selected product data. - */ - public function ajax_get_selected_product_data() { - check_ajax_referer( 'wc-stripe-get-selected-product-data', 'security' ); - - try { // @phpstan-ignore-line (return statement is added) - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; - $qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id ); - $addon_value = isset( $_POST['addon_value'] ) ? max( floatval( $_POST['addon_value'] ), 0 ) : 0; - $product = wc_get_product( $product_id ); - $variation_id = null; - - if ( ! is_a( $product, 'WC_Product' ) ) { - /* translators: 1) The product Id */ - throw new Exception( sprintf( __( 'Product with the ID (%1$s) cannot be found.', 'woocommerce-gateway-stripe' ), $product_id ) ); - } - - if ( in_array( $product->get_type(), [ 'variable', 'variable-subscription' ], true ) && isset( $_POST['attributes'] ) ) { - $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); - - $data_store = WC_Data_Store::load( 'product' ); - $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); - - if ( ! empty( $variation_id ) ) { - $product = wc_get_product( $variation_id ); - } - } - - if ( $this->is_invalid_subscription_product( $product, true ) ) { - throw new Exception( __( 'The chosen subscription product is not supported.', 'woocommerce-gateway-stripe' ) ); - } - - // Force quantity to 1 if sold individually and check for existing item in cart. - if ( $product->is_sold_individually() ) { - $qty = apply_filters( 'wc_stripe_payment_request_add_to_cart_sold_individually_quantity', 1, $qty, $product_id, $variation_id ); - } - - if ( ! $product->has_enough_stock( $qty ) ) { - /* translators: 1) product name 2) quantity in stock */ - throw new Exception( sprintf( __( 'You cannot add that amount of "%1$s"; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce-gateway-stripe' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) ); - } - - $total = $qty * $this->get_product_price( $product ) + $addon_value; - - $quantity_label = 1 < $qty ? ' (x' . $qty . ')' : ''; - - $data = []; - $items = []; - - $items[] = [ - 'label' => $product->get_name() . $quantity_label, - 'amount' => WC_Stripe_Helper::get_stripe_amount( $total ), - ]; - - if ( wc_tax_enabled() ) { - $items[] = [ - 'label' => __( 'Tax', 'woocommerce-gateway-stripe' ), - 'amount' => 0, - 'pending' => true, - ]; - } - - if ( wc_shipping_enabled() && $product->needs_shipping() ) { - $items[] = [ - 'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ), - 'amount' => 0, - 'pending' => true, - ]; - - $data['shippingOptions'] = [ - 'id' => 'pending', - 'label' => __( 'Pending', 'woocommerce-gateway-stripe' ), - 'detail' => '', - 'amount' => 0, - ]; - } - - $data['displayItems'] = $items; - $data['total'] = [ - 'label' => $this->total_label, - 'amount' => WC_Stripe_Helper::get_stripe_amount( $total ), - ]; - - $data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() ); - $data['currency'] = strtolower( get_woocommerce_currency() ); - $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); - - wp_send_json( $data ); - } catch ( Exception $e ) { - wp_send_json( [ 'error' => wp_strip_all_tags( $e->getMessage() ) ] ); - } - } - - /** - * Adds the current product to the cart. Used on product detail page. - * - * @return array $data Results of adding the product to the cart. - */ - public function ajax_add_to_cart() { - check_ajax_referer( 'wc-stripe-add-to-cart', 'security' ); - - if ( ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - } - - WC()->shipping->reset_shipping(); - - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; - $qty = ! isset( $_POST['qty'] ) ? 1 : absint( $_POST['qty'] ); - $product = wc_get_product( $product_id ); - $product_type = $product->get_type(); - - // First empty the cart to prevent wrong calculation. - WC()->cart->empty_cart(); - - if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) { - $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); - - $data_store = WC_Data_Store::load( 'product' ); - $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); - - WC()->cart->add_to_cart( $product->get_id(), $qty, $variation_id, $attributes ); - } - - if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation' ], true ) ) { - WC()->cart->add_to_cart( $product->get_id(), $qty ); - } - - WC()->cart->calculate_totals(); - - $data = []; - $data += $this->build_display_items(); - $data['result'] = 'success'; - - // @phpstan-ignore-next-line (return statement is added) - wp_send_json( $data ); - } - - /** - * Normalizes billing and shipping state fields. - */ - public function normalize_state() { - $billing_country = ! empty( $_POST['billing_country'] ) ? wc_clean( wp_unslash( $_POST['billing_country'] ) ) : ''; - $shipping_country = ! empty( $_POST['shipping_country'] ) ? wc_clean( wp_unslash( $_POST['shipping_country'] ) ) : ''; - $billing_state = ! empty( $_POST['billing_state'] ) ? wc_clean( wp_unslash( $_POST['billing_state'] ) ) : ''; - $shipping_state = ! empty( $_POST['shipping_state'] ) ? wc_clean( wp_unslash( $_POST['shipping_state'] ) ) : ''; - - // Due to a bug in Apple Pay, the "Region" part of a Hong Kong address is delivered in - // `shipping_postcode`, so we need some special case handling for that. According to - // our sources at Apple Pay people will sometimes use the district or even sub-district - // for this value. As such we check against all regions, districts, and sub-districts - // with both English and Mandarin spelling. - // - // @reykjalin: The check here is quite elaborate in an attempt to make sure this doesn't break once - // Apple Pay fixes the bug that causes address values to be in the wrong place. Because of that the - // algorithm becomes: - // 1. Use the supplied state if it's valid (in case Apple Pay bug is fixed) - // 2. Use the value supplied in the postcode if it's a valid HK region (equivalent to a WC state). - // 3. Fall back to the value supplied in the state. This will likely cause a validation error, in - // which case a merchant can reach out to us so we can either: 1) add whatever the customer used - // as a state to our list of valid states; or 2) let them know the customer must spell the state - // in some way that matches our list of valid states. - // - // @reykjalin: This HK specific sanitazation *should be removed* once Apple Pay fix - // the address bug. More info on that in pc4etw-bY-p2. - if ( 'HK' === $billing_country ) { - include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-hong-kong-states.php'; - - if ( ! WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $billing_state ) ) ) { - $billing_postcode = ! empty( $_POST['billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['billing_postcode'] ) ) : ''; - if ( WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $billing_postcode ) ) ) { - $billing_state = $billing_postcode; - } - } - } - if ( 'HK' === $shipping_country ) { - include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-hong-kong-states.php'; - - if ( ! WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $shipping_state ) ) ) { - $shipping_postcode = ! empty( $_POST['shipping_postcode'] ) ? wc_clean( wp_unslash( $_POST['shipping_postcode'] ) ) : ''; - if ( WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $shipping_postcode ) ) ) { - $shipping_state = $shipping_postcode; - } - } - } - - // Finally we normalize the state value we want to process. - if ( $billing_state && $billing_country ) { - $_POST['billing_state'] = $this->get_normalized_state( $billing_state, $billing_country ); - } - - if ( $shipping_state && $shipping_country ) { - $_POST['shipping_state'] = $this->get_normalized_state( $shipping_state, $shipping_country ); - } - } - - /** - * Checks if given state is normalized. - * - * @param string $state State. - * @param string $country Two-letter country code. - * - * @return bool Whether state is normalized or not. - */ - public function is_normalized_state( $state, $country ) { - $wc_states = WC()->countries->get_states( $country ); - return ( - is_array( $wc_states ) && - in_array( $state, array_keys( $wc_states ), true ) - ); - } - - /** - * Sanitize string for comparison. - * - * @param string $string String to be sanitized. - * - * @return string The sanitized string. - */ - public function sanitize_string( $string ) { - return trim( wc_strtolower( remove_accents( $string ) ) ); - } - - /** - * Get normalized state from express checkout API dropdown list of states. - * - * @param string $state Full state name or state code. - * @param string $country Two-letter country code. - * - * @return string Normalized state or original state input value. - */ - public function get_normalized_state_from_pr_states( $state, $country ) { - // Include Payment Request API State list for compatibility with WC countries/states. - include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-payment-request-button-states.php'; - $pr_states = WC_Stripe_Payment_Request_Button_States::STATES; - - if ( ! isset( $pr_states[ $country ] ) ) { - return $state; - } - - foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) { - $sanitized_state_string = $this->sanitize_string( $state ); - // Checks if input state matches with Payment Request state code (0), name (1) or localName (2). - if ( - ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) || - ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) || - ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) ) - ) { - return $wc_state_abbr; - } - } - - return $state; - } - - /** - * Get normalized state from WooCommerce list of translated states. - * - * @param string $state Full state name or state code. - * @param string $country Two-letter country code. - * - * @return string Normalized state or original state input value. - */ - public function get_normalized_state_from_wc_states( $state, $country ) { - $wc_states = WC()->countries->get_states( $country ); - - if ( is_array( $wc_states ) ) { - foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) { - if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) { - return $wc_state_abbr; - } - } - } - - return $state; - } - - /** - * Gets the normalized state/county field because in some - * cases, the state/county field is formatted differently from - * what WC is expecting and throws an error. An example - * for Ireland, the county dropdown in Chrome shows "Co. Clare" format. - * - * @param string $state Full state name or an already normalized abbreviation. - * @param string $country Two-letter country code. - * - * @return string Normalized state abbreviation. - */ - public function get_normalized_state( $state, $country ) { - // If it's empty or already normalized, skip. - if ( ! $state || $this->is_normalized_state( $state, $country ) ) { - return $state; - } - - // Try to match state from the Payment Request API list of states. - $state = $this->get_normalized_state_from_pr_states( $state, $country ); - - // If it's normalized, return. - if ( $this->is_normalized_state( $state, $country ) ) { - return $state; - } - - // If the above doesn't work, fallback to matching against the list of translated - // states from WooCommerce. - return $this->get_normalized_state_from_wc_states( $state, $country ); - } - - /** - * The express checkout API provides its own validation for the address form. - * For some countries, it might not provide a state field, so we need to return a more descriptive - * error message, indicating that the express checkout button is not supported for that country. - */ - public function validate_state() { - $wc_checkout = WC_Checkout::instance(); - $posted_data = $wc_checkout->get_posted_data(); - $checkout_fields = $wc_checkout->get_checkout_fields(); - $countries = WC()->countries->get_countries(); - - $is_supported = true; - // Checks if billing state is missing and is required. - if ( ! empty( $checkout_fields['billing']['billing_state']['required'] ) && '' === $posted_data['billing_state'] ) { - $is_supported = false; - } - - // Checks if shipping state is missing and is required. - if ( WC()->cart->needs_shipping_address() && ! empty( $checkout_fields['shipping']['shipping_state']['required'] ) && '' === $posted_data['shipping_state'] ) { - $is_supported = false; - } - - if ( ! $is_supported ) { - wc_add_notice( - sprintf( - /* translators: 1) country. */ - __( 'The Express Checkout button is not supported in %1$s because some required fields couldn\'t be verified. Please proceed to the checkout page and try again.', 'woocommerce-gateway-stripe' ), - isset( $countries[ $posted_data['billing_country'] ] ) ? $countries[ $posted_data['billing_country'] ] : $posted_data['billing_country'] - ), - 'error' - ); - } - } - - /** - * Create order. Security is handled by WC. - */ - public function ajax_create_order() { - if ( WC()->cart->is_empty() ) { - wp_send_json_error( __( 'Empty cart', 'woocommerce-gateway-stripe' ) ); - } - - if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { - define( 'WOOCOMMERCE_CHECKOUT', true ); - } - - // Normalizes billing and shipping state values. - $this->normalize_state(); - - // In case the state is required, but is missing, add a more descriptive error notice. - $this->validate_state(); - - WC()->checkout()->process_checkout(); - - die( 0 ); - } - - /** - * Calculate and set shipping method. - * - * @param array $address Shipping address. - */ - protected function calculate_shipping( $address = [] ) { - $country = $address['country']; - $state = $address['state']; - $postcode = $address['postcode']; - $city = $address['city']; - $address_1 = $address['address']; - $address_2 = $address['address_2']; - - // Normalizes state to calculate shipping zones. - $state = $this->get_normalized_state( $state, $country ); - - // Normalizes postal code in case of redacted data from Apple Pay. - $postcode = $this->get_normalized_postal_code( $postcode, $country ); - - WC()->shipping->reset_shipping(); - - if ( $postcode && WC_Validation::is_postcode( $postcode, $country ) ) { - $postcode = wc_format_postcode( $postcode, $country ); - } - - if ( $country ) { - WC()->customer->set_location( $country, $state, $postcode, $city ); - WC()->customer->set_shipping_location( $country, $state, $postcode, $city ); - } else { - WC()->customer->set_billing_address_to_base(); - WC()->customer->set_shipping_address_to_base(); - } - - WC()->customer->set_calculated_shipping( true ); - WC()->customer->save(); - - $packages = []; - - $packages[0]['contents'] = WC()->cart->get_cart(); - $packages[0]['contents_cost'] = 0; - $packages[0]['applied_coupons'] = WC()->cart->applied_coupons; - $packages[0]['user']['ID'] = get_current_user_id(); - $packages[0]['destination']['country'] = $country; - $packages[0]['destination']['state'] = $state; - $packages[0]['destination']['postcode'] = $postcode; - $packages[0]['destination']['city'] = $city; - $packages[0]['destination']['address'] = $address_1; - $packages[0]['destination']['address_2'] = $address_2; - - foreach ( WC()->cart->get_cart() as $item ) { - if ( $item['data']->needs_shipping() ) { - if ( isset( $item['line_total'] ) ) { - $packages[0]['contents_cost'] += $item['line_total']; - } - } - } - - $packages = apply_filters( 'woocommerce_cart_shipping_packages', $packages ); - - WC()->shipping->calculate_shipping( $packages ); - } - - /** - * The settings for the `button` attribute. - * - * @return array - */ - public function get_button_settings() { - $button_type = $this->get_button_type(); - return [ - 'type' => $button_type, - 'theme' => $this->get_button_theme(), - 'height' => $this->get_button_height(), - // Default format is en_US. - 'locale' => apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) ), - ]; - } - - /** - * Builds the shippings methods to pass to express checkout elements. - */ - protected function build_shipping_methods( $shipping_methods ) { - if ( empty( $shipping_methods ) ) { - return []; - } - - $shipping = []; - - foreach ( $shipping_methods as $method ) { - $shipping[] = [ - 'id' => $method['id'], - 'label' => $method['label'], - 'detail' => '', - 'amount' => WC_Stripe_Helper::get_stripe_amount( $method['amount']['value'] ), - ]; - } - - return $shipping; - } - - /** - * Builds the line items to pass to express checkout elements. - */ - protected function build_display_items( $itemized_display_items = false ) { - if ( ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - } - - $items = []; - $lines = []; - $subtotal = 0; - $discounts = 0; - $display_items = ! apply_filters( 'wc_stripe_payment_request_hide_itemization', true ) || $itemized_display_items; - - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - $subtotal += $cart_item['line_subtotal']; - $amount = $cart_item['line_subtotal']; - $quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : ''; - $product_name = $cart_item['data']->get_name(); - - $lines[] = [ - 'label' => $product_name . $quantity_label, - 'amount' => WC_Stripe_Helper::get_stripe_amount( $amount ), - ]; - } - - if ( $display_items ) { - $items = array_merge( $items, $lines ); - } else { - // Default show only subtotal instead of itemization. - - $items[] = [ - 'label' => 'Subtotal', - 'amount' => WC_Stripe_Helper::get_stripe_amount( $subtotal ), - ]; - } - - $applied_coupons = array_values( WC()->cart->get_coupon_discount_totals() ); - - foreach ( $applied_coupons as $amount ) { - $discounts += (float) $amount; - } - - $discounts = wc_format_decimal( $discounts, WC()->cart->dp ); - $tax = wc_format_decimal( WC()->cart->tax_total + WC()->cart->shipping_tax_total, WC()->cart->dp ); - $shipping = wc_format_decimal( WC()->cart->shipping_total, WC()->cart->dp ); - $items_total = wc_format_decimal( WC()->cart->cart_contents_total, WC()->cart->dp ) + $discounts; - $order_total = WC()->cart->get_total( false ); - - if ( wc_tax_enabled() ) { - $items[] = [ - 'label' => esc_html( __( 'Tax', 'woocommerce-gateway-stripe' ) ), - 'amount' => WC_Stripe_Helper::get_stripe_amount( $tax ), - ]; - } - - if ( WC()->cart->needs_shipping() ) { - $items[] = [ - 'label' => esc_html( __( 'Shipping', 'woocommerce-gateway-stripe' ) ), - 'amount' => WC_Stripe_Helper::get_stripe_amount( $shipping ), - ]; - } - - if ( WC()->cart->has_discount() ) { - $items[] = [ - 'label' => esc_html( __( 'Discount', 'woocommerce-gateway-stripe' ) ), - 'amount' => WC_Stripe_Helper::get_stripe_amount( $discounts ), - ]; - } - - $cart_fees = WC()->cart->get_fees(); - - // Include fees and taxes as display items. - foreach ( $cart_fees as $key => $fee ) { - $items[] = [ - 'label' => $fee->name, - 'amount' => WC_Stripe_Helper::get_stripe_amount( $fee->amount ), - ]; - } - - return [ - 'displayItems' => $items, - 'total' => [ - 'label' => $this->total_label, - 'amount' => max( 0, apply_filters( 'woocommerce_stripe_calculated_total', WC_Stripe_Helper::get_stripe_amount( $order_total ), $order_total, WC()->cart ) ), - 'pending' => false, - ], - ]; - } - - /** - * Settings array for the user authentication dialog and redirection. - * - * @return array - */ - public function get_login_confirmation_settings() { - if ( is_user_logged_in() || ! $this->is_authentication_required() ) { - return false; - } - - /* translators: The text encapsulated in `**` can be replaced with "Apple Pay" or "Google Pay". Please translate this text, but don't remove the `**`. */ - $message = __( 'To complete your transaction with **the selected payment method**, you must log in or create an account with our site.', 'woocommerce-gateway-stripe' ); - $redirect_url = add_query_arg( - [ - '_wpnonce' => wp_create_nonce( 'wc-stripe-set-redirect-url' ), - 'wc_stripe_express_checkout_redirect_url' => rawurlencode( home_url( add_query_arg( [] ) ) ), // Current URL to redirect to after login. - ], - home_url() - ); - - return [ - 'message' => $message, - 'redirect_url' => wp_sanitize_redirect( esc_url_raw( $redirect_url ) ), - ]; - } - - /** - * Pages where the express checkout buttons should be displayed. - * - * @return array - */ - public function get_button_locations() { - // If the locations have not been set return the default setting. - if ( ! isset( $this->stripe_settings['payment_request_button_locations'] ) ) { - return [ 'product', 'cart' ]; + if ( ! $this->express_checkout_helper->is_page_supported() ) { + return; } - // If all locations are removed through the settings UI the location config will be set to - // an empty string "". If that's the case (and if the settings are not an array for any - // other reason) we should return an empty array. - if ( ! is_array( $this->stripe_settings['payment_request_button_locations'] ) ) { - return []; + if ( ! $this->express_checkout_helper->should_show_express_checkout_button() ) { + return; } - return $this->stripe_settings['payment_request_button_locations']; - } - - /** - * Returns whether Stripe express checkout element is enabled. - * - * This option defines whether Apple Pay and Google Pay buttons are enabled. - * - * @return boolean - */ - private function is_express_checkout_enabled() { - return isset( $this->stripe_settings['payment_request'] ) && 'yes' === $this->stripe_settings['payment_request']; + ?> + + display_express_checkout_button_separator_html(); } /** - * Restores the shipping methods previously chosen for each recurring cart after shipping was reset and recalculated - * during the express checkout get_shipping_options flow. - * - * When the cart contains multiple subscriptions with different billing periods, customers are able to select different shipping - * methods for each subscription, however, this is not supported when purchasing with Apple Pay and Google Pay as it's - * only concerned about handling the initial purchase. - * - * In order to avoid Woo Subscriptions's `WC_Subscriptions_Cart::validate_recurring_shipping_methods` throwing an error, we need to restore - * the previously chosen shipping methods for each recurring cart. - * - * This function needs to be called after `WC()->cart->calculate_totals()` is run, otherwise `WC()->cart->recurring_carts` won't exist yet. - * - * @param array $previous_chosen_methods The previously chosen shipping methods. + * Display express checkout button separator. */ - private function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) { - if ( empty( WC()->cart->recurring_carts ) || ! method_exists( 'WC_Subscriptions_Cart', 'get_recurring_shipping_package_key' ) ) { + public function display_express_checkout_button_separator_html() { + if ( ! is_checkout() && ! is_wc_endpoint_url( 'order-pay' ) ) { return; } - $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); - - foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) { - foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_index => $recurring_cart_package ) { - if ( class_exists( 'WC_Subscriptions_Cart' ) ) { - $package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $recurring_cart_key, $recurring_cart_package_index ); - - // If the recurring cart package key is found in the previous chosen methods, but not in the current chosen methods, restore it. - if ( isset( $previous_chosen_methods[ $package_key ] ) && ! isset( $chosen_shipping_methods[ $package_key ] ) ) { - $chosen_shipping_methods[ $package_key ] = $previous_chosen_methods[ $package_key ]; - } - } - } + if ( is_checkout() && ! in_array( 'checkout', $this->express_checkout_helper->get_button_locations(), true ) ) { + return; } - WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + ?> + + stripe_settings = WC_Stripe_Helper::get_stripe_settings(); + $this->testmode = ( ! empty( $this->stripe_settings['testmode'] ) && 'yes' === $this->stripe_settings['testmode'] ) ? true : false; + $this->total_label = ! empty( $this->stripe_settings['statement_descriptor'] ) ? WC_Stripe_Helper::clean_statement_descriptor( $this->stripe_settings['statement_descriptor'] ) : ''; + + $this->total_label = str_replace( "'", '', $this->total_label ) . apply_filters( 'wc_stripe_payment_request_total_label_suffix', ' (via WooCommerce)' ); + + } + + /** + * Checks whether authentication is required for checkout. + * + * @return bool + */ + public function is_authentication_required() { + // If guest checkout is disabled and account creation upon checkout is not possible, authentication is required. + if ( 'no' === get_option( 'woocommerce_enable_guest_checkout', 'yes' ) && ! $this->is_account_creation_possible() ) { + return true; + } + // If cart contains subscription and account creation upon checkout is not posible, authentication is required. + if ( $this->has_subscription_product() && ! $this->is_account_creation_possible() ) { + return true; + } + + return false; + } + + /** + * Checks whether account creation is possible upon checkout. + * + * @return bool + */ + public function is_account_creation_possible() { + // If automatically generate username/password are disabled, we can not include any of those fields, + // during express checkout. So account creation is not possible. + return ( + 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'no' ) && + 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && + 'yes' === get_option( 'woocommerce_registration_generate_password', 'yes' ) + ); + } + + /** + * Gets the button type. + * + * @return string + */ + public function get_button_type() { + return isset( $this->stripe_settings['payment_request_button_type'] ) ? $this->stripe_settings['payment_request_button_type'] : 'default'; + } + + /** + * Gets the button theme. + * + * @return string + */ + public function get_button_theme() { + return isset( $this->stripe_settings['payment_request_button_theme'] ) ? $this->stripe_settings['payment_request_button_theme'] : 'dark'; + } + + /** + * Gets the button height. + * + * @return string + */ + public function get_button_height() { + $height = isset( $this->stripe_settings['payment_request_button_size'] ) ? $this->stripe_settings['payment_request_button_size'] : 'default'; + if ( 'small' === $height ) { + return '40'; + } + + if ( 'large' === $height ) { + return '56'; + } + + return '48'; + } + + /** + * Gets total label. + * + * @return string + */ + public function get_total_label() { + return $this->total_label; + } + + /** + * Gets the product total price. + * + * @param object $product WC_Product_* object. + * @return integer Total price. + */ + public function get_product_price( $product ) { + $product_price = $product->get_price(); + // Add subscription sign-up fees to product price. + if ( in_array( $product->get_type(), [ 'subscription', 'subscription_variation' ] ) && class_exists( 'WC_Subscriptions_Product' ) ) { + $product_price = $product->get_price() + WC_Subscriptions_Product::get_sign_up_fee( $product ); + } + + return $product_price; + } + + /** + * Gets the product data for the currently viewed page + * + * @return mixed Returns false if not on a product page, the product information otherwise. + */ + public function get_product_data() { + if ( ! $this->is_product() ) { + return false; + } + + $product = $this->get_product(); + $variation_id = 0; + + if ( in_array( $product->get_type(), [ 'variable', 'variable-subscription' ], true ) ) { + $variation_attributes = $product->get_variation_attributes(); + $attributes = []; + + foreach ( $variation_attributes as $attribute_name => $attribute_values ) { + $attribute_key = 'attribute_' . sanitize_title( $attribute_name ); + + // Passed value via GET takes precedence, then POST, otherwise get the default value for given attribute + if ( isset( $_GET[ $attribute_key ] ) ) { + $attributes[ $attribute_key ] = wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ); + } elseif ( isset( $_POST[ $attribute_key ] ) ) { + $attributes[ $attribute_key ] = wc_clean( wp_unslash( $_POST[ $attribute_key ] ) ); + } else { + $attributes[ $attribute_key ] = $product->get_variation_default_attribute( $attribute_name ); + } + } + + $data_store = WC_Data_Store::load( 'product' ); + $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); + + if ( ! empty( $variation_id ) ) { + $product = wc_get_product( $variation_id ); + } + } + + $data = []; + $items = []; + + $items[] = [ + 'label' => $product->get_name(), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $this->get_product_price( $product ) ), + ]; + + if ( wc_tax_enabled() ) { + $items[] = [ + 'label' => __( 'Tax', 'woocommerce-gateway-stripe' ), + 'amount' => 0, + 'pending' => true, + ]; + } + + if ( wc_shipping_enabled() && $product->needs_shipping() ) { + $items[] = [ + 'label' => __( 'Shipping', 'woocommerce-gateway-stripe' ), + 'amount' => 0, + 'pending' => true, + ]; + + $data['shippingOptions'] = [ + 'id' => 'pending', + 'label' => __( 'Pending', 'woocommerce-gateway-stripe' ), + 'detail' => '', + 'amount' => 0, + ]; + } + + $data['displayItems'] = $items; + $data['total'] = [ + 'label' => apply_filters( 'wc_stripe_payment_request_total_label', $this->total_label ), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $this->get_product_price( $product ) ), + ]; + + $data['requestShipping'] = ( wc_shipping_enabled() && $product->needs_shipping() && 0 !== wc_get_shipping_method_count( true ) ); + $data['currency'] = strtolower( get_woocommerce_currency() ); + $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); + + // On product page load, if there's a variation already selected, check if it's supported. + $data['validVariationSelected'] = ! empty( $variation_id ) ? $this->is_product_supported( $product ) : true; + + return apply_filters( 'wc_stripe_payment_request_product_data', $data, $product ); + } + + /** + * Normalizes postal code in case of redacted data from Apple Pay. + * + * @param string $postcode Postal code. + * @param string $country Country. + */ + public function get_normalized_postal_code( $postcode, $country ) { + /** + * Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively + * when passing it back from the shippingcontactselected object. This causes WC to invalidate + * the postal code and not calculate shipping zones correctly. + */ + if ( 'GB' === $country ) { + // Replaces a redacted string with something like LN10***. + return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '*' ); + } + if ( 'CA' === $country ) { + // Replaces a redacted string with something like L4Y***. + return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '*' ); + } + + return $postcode; + } + + /** + * Checks to make sure product type is supported. + * + * @return array + */ + public function supported_product_types() { + return apply_filters( + 'wc_stripe_payment_request_supported_types', + [ + 'simple', + 'variable', + 'variation', + 'subscription', + 'variable-subscription', + 'subscription_variation', + 'booking', + 'bundle', + 'composite', + ] + ); + } + + /** + * Checks the cart to see if all items are allowed to be used. + * + * @return boolean + */ + public function allowed_items_in_cart() { + // Pre Orders compatibility where we don't support charge upon release. + if ( $this->is_pre_order_item_in_cart() && $this->is_pre_order_product_charged_upon_release( $this->get_pre_order_product_from_cart() ) ) { + return false; + } + + // If the cart is not available we don't have any unsupported products in the cart, so we + // return true. This can happen e.g. when loading the cart or checkout blocks in Gutenberg. + if ( is_null( WC()->cart ) ) { + return true; + } + + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); + + if ( ! in_array( $_product->get_type(), $this->supported_product_types() ) ) { + return false; + } + + // Subscriptions with a trial period that need shipping are not supported. + if ( $this->is_invalid_subscription_product( $_product ) ) { + return false; + } + } + + // We don't support multiple packages with express checkout buttons because we can't offer + // a good UX. + $packages = WC()->cart->get_shipping_packages(); + if ( 1 < count( $packages ) ) { + return false; + } + + return true; + } + + /** + * Returns true if the given product is a subscription that cannot be purchased with express checkout buttons. + * + * Invalid subscription products include those with: + * - a free trial that requires shipping (synchronised subscriptions with a delayed first payment are considered to have a free trial) + * - a synchronised subscription with no upfront payment and is virtual (this limitation only applies to the product page as we cannot calculate totals correctly) + * + * If the product is a variable subscription, this function will return true if all of its variations have a trial and require shipping. + * + * @since 7.8.0 + * + * @param WC_Product|null $product Product object. + * @param boolean $is_product_page_request Whether this is a request from the product page. + * + * @return boolean + */ + public function is_invalid_subscription_product( $product, $is_product_page_request = false ) { + if ( ! class_exists( 'WC_Subscriptions_Product' ) || ! class_exists( 'WC_Subscriptions_Synchroniser' ) || ! WC_Subscriptions_Product::is_subscription( $product ) ) { + return false; + } + + $is_invalid = true; + + if ( $product->get_type() === 'variable-subscription' ) { + $products = $product->get_available_variations( 'object' ); + } else { + $products = [ $product ]; + } + + foreach ( $products as $product ) { + $needs_shipping = $product->needs_shipping(); + $is_synced = WC_Subscriptions_Synchroniser::is_product_synced( $product ); + $is_payment_upfront = WC_Subscriptions_Synchroniser::is_payment_upfront( $product ); + $has_trial_period = WC_Subscriptions_Product::get_trial_length( $product ) > 0; + + if ( $is_product_page_request && $is_synced && ! $is_payment_upfront && ! $needs_shipping ) { + /** + * This condition prevents the purchase of virtual synced subscription products with no upfront costs via express checkout buttons from the product page. + * + * The main issue is that calling $product->get_price() on a synced subscription does not take into account a mock trial period or prorated price calculations + * until the product is in the cart. This means that the totals passed to express checkout element are incorrect when purchasing from the product page. + * Another part of the problem is because the product is virtual this stops the Stripe PaymentRequest API from triggering the necessary `shippingaddresschange` event + * which is when we call WC()->cart->calculate_totals(); which would fix the totals. + * + * The fix here is to not allow virtual synced subscription products with no upfront costs to be purchased via express checkout buttons on the product page. + */ + continue; + } elseif ( $is_synced && ! $is_payment_upfront && $needs_shipping ) { + continue; + } elseif ( $has_trial_period && $needs_shipping ) { + continue; + } else { + // If we made it this far, the product is valid. Break out of the foreach and return early as we only care about invalid cases. + $is_invalid = false; + break; + } + } + + return $is_invalid; + } + + /** + * Checks whether cart contains a subscription product or this is a subscription product page. + * + * @return boolean + */ + public function has_subscription_product() { + if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { + return false; + } + + if ( $this->is_product() ) { + $product = $this->get_product(); + if ( WC_Subscriptions_Product::is_subscription( $product ) ) { + return true; + } + } elseif ( WC_Stripe_Helper::has_cart_or_checkout_on_current_page() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); + if ( WC_Subscriptions_Product::is_subscription( $_product ) ) { + return true; + } + } + } + + return false; + } + + /** + * Checks if this is a product page or content contains a product_page shortcode. + * + * @return boolean + */ + public function is_product() { + return is_product() || wc_post_content_has_shortcode( 'product_page' ); + } + + /** + * Get product from product page or product_page shortcode. + * + * @return WC_Product Product object. + */ + public function get_product() { + global $post; + + if ( is_product() ) { + return wc_get_product( $post->ID ); + } elseif ( wc_post_content_has_shortcode( 'product_page' ) ) { + // Get id from product_page shortcode. + preg_match( '/\[product_page id="(?\d+)"\]/', $post->post_content, $shortcode_match ); + + if ( ! isset( $shortcode_match['id'] ) ) { + return false; + } + + return wc_get_product( $shortcode_match['id'] ); + } + + return false; + } + + /** + * Returns true if the current page supports Express Checkout Buttons, false otherwise. + * + * @return boolean True if the current page is supported, false otherwise. + */ + private function is_page_supported() { + return $this->is_product() + || WC_Stripe_Helper::has_cart_or_checkout_on_current_page() + || is_wc_endpoint_url( 'order-pay' ); + } + + /** + * Returns true if express checkout elements are supported on the current page, false + * otherwise. + * + * @return boolean True if express checkout elements are supported on current page, false otherwise + */ + public function should_show_express_checkout_button() { + // Bail if account is not connected. + if ( ! WC_Stripe::get_instance()->connect->is_connected() ) { + WC_Stripe_Logger::log( 'Account is not connected.' ); + return false; + } + + // If no SSL bail. + if ( ! $this->testmode && ! is_ssl() ) { + WC_Stripe_Logger::log( 'Stripe Express Checkout live mode requires SSL.' ); + return false; + } + + // Don't show if on the cart or checkout page, or if page contains the cart or checkout + // shortcodes, with items in the cart that aren't supported. + if ( + WC_Stripe_Helper::has_cart_or_checkout_on_current_page() + && ! $this->allowed_items_in_cart() + ) { + return false; + } + + // Don't show on cart if disabled. + if ( is_cart() && ! $this->should_show_ece_on_cart_page() ) { + return false; + } + + // Don't show on checkout if disabled. + if ( is_checkout() && ! $this->should_show_ece_on_checkout_page() ) { + return false; + } + + // Don't show if product page PRB is disabled. + if ( $this->is_product() && ! $this->should_show_ece_on_product_pages() ) { + return false; + } + + // Don't show if product on current page is not supported. + if ( $this->is_product() && ! $this->is_product_supported( $this->get_product() ) ) { + return false; + } + + if ( $this->is_product() && in_array( $this->get_product()->get_type(), [ 'variable', 'variable-subscription' ], true ) ) { + $stock_availability = array_column( $this->get_product()->get_available_variations(), 'is_in_stock' ); + // Don't show if all product variations are out-of-stock. + if ( ! in_array( true, $stock_availability, true ) ) { + return false; + } + } + + return true; + } + + /** + * Returns true if express checkout buttons are enabled on the cart page, false + * otherwise. + * + * @return boolean True if express checkout buttons are enabled on the cart page, false otherwise + */ + public function should_show_ece_on_cart_page() { + $should_show_on_cart_page = in_array( 'cart', $this->get_button_locations(), true ); + + return apply_filters( + 'wc_stripe_show_payment_request_on_cart', + $should_show_on_cart_page + ); + } + + /** + * Returns true if express checkout buttons are enabled on the checkout page, false + * otherwise. + * + * @return boolean True if express checkout buttons are enabled on the checkout page, false otherwise + */ + public function should_show_ece_on_checkout_page() { + global $post; + + $should_show_on_checkout_page = in_array( 'checkout', $this->get_button_locations(), true ); + + return apply_filters( + 'wc_stripe_show_payment_request_on_checkout', + $should_show_on_checkout_page, + $post + ); + } + + /** + * Returns true if express checkout buttons are enabled on product pages, false + * otherwise. + * + * @return boolean True if express checkout buttons are enabled on product pages, false otherwise + */ + public function should_show_ece_on_product_pages() { + global $post; + + $should_show_on_product_page = in_array( 'product', $this->get_button_locations(), true ); + + // Note the negation because if the filter returns `true` that means we should hide the PRB. + return ! apply_filters( + 'wc_stripe_hide_payment_request_on_product_page', + ! $should_show_on_product_page, + $post + ); + } + + /** + * Returns true if a the provided product is supported, false otherwise. + * + * @param WC_Product $param The product that's being checked for support. + * + * @return boolean True if the provided product is supported, false otherwise. + */ + private function is_product_supported( $product ) { + if ( ! is_object( $product ) || ! in_array( $product->get_type(), $this->supported_product_types() ) ) { + return false; + } + + // Trial subscriptions with shipping are not supported. + if ( $this->is_invalid_subscription_product( $product, true ) ) { + return false; + } + + // Pre Orders charge upon release not supported. + if ( $this->is_pre_order_product_charged_upon_release( $product ) ) { + return false; + } + + // Composite products are not supported on the product page. + if ( class_exists( 'WC_Composite_Products' ) && function_exists( 'is_composite_product' ) && is_composite_product() ) { + return false; + } + + // File upload addon not supported + if ( class_exists( 'WC_Product_Addons_Helper' ) ) { + $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() ); + foreach ( $product_addons as $addon ) { + if ( 'file_upload' === $addon['type'] ) { + return false; + } + } + } + + return true; + } + + /** + * Gets shipping options available for specified shipping address + * + * @param array $shipping_address Shipping address. + * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views. + * + * @return array Shipping options data. + * + * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag + */ + public function get_shipping_options( $shipping_address, $itemized_display_items = false ) { + try { + // Set the shipping options. + $data = []; + + // Remember current shipping method before resetting. + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); + $this->calculate_shipping( apply_filters( 'wc_stripe_payment_request_shipping_posted_values', $shipping_address ) ); + + $packages = WC()->shipping->get_packages(); + $shipping_rate_ids = []; + + if ( ! empty( $packages ) && WC()->customer->has_calculated_shipping() ) { + foreach ( $packages as $package_key => $package ) { + if ( empty( $package['rates'] ) ) { + throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-gateway-stripe' ) ); + } + + foreach ( $package['rates'] as $key => $rate ) { + if ( in_array( $rate->id, $shipping_rate_ids, true ) ) { + // The express checkout will try to load indefinitely if there are duplicate shipping + // option IDs. + throw new Exception( __( 'Unable to provide shipping options for express checkout.', 'woocommerce-gateway-stripe' ) ); + } + $shipping_rate_ids[] = $rate->id; + $data['shipping_options'][] = [ + 'id' => $rate->id, + 'label' => $rate->label, + 'detail' => '', + 'amount' => WC_Stripe_Helper::get_stripe_amount( $rate->cost ), + ]; + } + } + } else { + throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-gateway-stripe' ) ); + } + + // The first shipping option is automatically applied on the client. + // Keep chosen shipping method by sorting shipping options if the method still available for new address. + // Fallback to the first available shipping method. + if ( isset( $data['shipping_options'][0] ) ) { + if ( isset( $chosen_shipping_methods[0] ) ) { + $chosen_method_id = $chosen_shipping_methods[0]; + $compare_shipping_options = function ( $a, $b ) use ( $chosen_method_id ) { + if ( $a['id'] === $chosen_method_id ) { + return -1; + } + + if ( $b['id'] === $chosen_method_id ) { + return 1; + } + + return 0; + }; + usort( $data['shipping_options'], $compare_shipping_options ); + } + + $first_shipping_method_id = $data['shipping_options'][0]['id']; + $this->update_shipping_method( [ $first_shipping_method_id ] ); + } + + WC()->cart->calculate_totals(); + + $this->maybe_restore_recurring_chosen_shipping_methods( $chosen_shipping_methods ); + + $data += $this->build_display_items( $itemized_display_items ); + $data['result'] = 'success'; + } catch ( Exception $e ) { + $data += $this->build_display_items( $itemized_display_items ); + $data['result'] = 'invalid_shipping_address'; + } + + return $data; + } + + /** + * Updates shipping method in WC session + * + * @param array $shipping_methods Array of selected shipping methods ids. + */ + public function update_shipping_method( $shipping_methods ) { + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); + + if ( is_array( $shipping_methods ) ) { + foreach ( $shipping_methods as $i => $value ) { + $chosen_shipping_methods[ $i ] = wc_clean( $value ); + } + } + + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } + + /** + * Normalizes billing and shipping state fields. + */ + public function normalize_state() { + $billing_country = ! empty( $_POST['billing_country'] ) ? wc_clean( wp_unslash( $_POST['billing_country'] ) ) : ''; + $shipping_country = ! empty( $_POST['shipping_country'] ) ? wc_clean( wp_unslash( $_POST['shipping_country'] ) ) : ''; + $billing_state = ! empty( $_POST['billing_state'] ) ? wc_clean( wp_unslash( $_POST['billing_state'] ) ) : ''; + $shipping_state = ! empty( $_POST['shipping_state'] ) ? wc_clean( wp_unslash( $_POST['shipping_state'] ) ) : ''; + + // Due to a bug in Apple Pay, the "Region" part of a Hong Kong address is delivered in + // `shipping_postcode`, so we need some special case handling for that. According to + // our sources at Apple Pay people will sometimes use the district or even sub-district + // for this value. As such we check against all regions, districts, and sub-districts + // with both English and Mandarin spelling. + // + // @reykjalin: The check here is quite elaborate in an attempt to make sure this doesn't break once + // Apple Pay fixes the bug that causes address values to be in the wrong place. Because of that the + // algorithm becomes: + // 1. Use the supplied state if it's valid (in case Apple Pay bug is fixed) + // 2. Use the value supplied in the postcode if it's a valid HK region (equivalent to a WC state). + // 3. Fall back to the value supplied in the state. This will likely cause a validation error, in + // which case a merchant can reach out to us so we can either: 1) add whatever the customer used + // as a state to our list of valid states; or 2) let them know the customer must spell the state + // in some way that matches our list of valid states. + // + // @reykjalin: This HK specific sanitazation *should be removed* once Apple Pay fix + // the address bug. More info on that in pc4etw-bY-p2. + if ( 'HK' === $billing_country ) { + include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-hong-kong-states.php'; + + if ( ! WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $billing_state ) ) ) { + $billing_postcode = ! empty( $_POST['billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['billing_postcode'] ) ) : ''; + if ( WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $billing_postcode ) ) ) { + $billing_state = $billing_postcode; + } + } + } + if ( 'HK' === $shipping_country ) { + include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-hong-kong-states.php'; + + if ( ! WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $shipping_state ) ) ) { + $shipping_postcode = ! empty( $_POST['shipping_postcode'] ) ? wc_clean( wp_unslash( $_POST['shipping_postcode'] ) ) : ''; + if ( WC_Stripe_Hong_Kong_States::is_valid_state( strtolower( $shipping_postcode ) ) ) { + $shipping_state = $shipping_postcode; + } + } + } + + // Finally we normalize the state value we want to process. + if ( $billing_state && $billing_country ) { + $_POST['billing_state'] = $this->get_normalized_state( $billing_state, $billing_country ); + } + + if ( $shipping_state && $shipping_country ) { + $_POST['shipping_state'] = $this->get_normalized_state( $shipping_state, $shipping_country ); + } + } + + /** + * Checks if given state is normalized. + * + * @param string $state State. + * @param string $country Two-letter country code. + * + * @return bool Whether state is normalized or not. + */ + public function is_normalized_state( $state, $country ) { + $wc_states = WC()->countries->get_states( $country ); + return ( + is_array( $wc_states ) && + in_array( $state, array_keys( $wc_states ), true ) + ); + } + + /** + * Sanitize string for comparison. + * + * @param string $string String to be sanitized. + * + * @return string The sanitized string. + */ + public function sanitize_string( $string ) { + return trim( wc_strtolower( remove_accents( $string ) ) ); + } + + /** + * Get normalized state from express checkout API dropdown list of states. + * + * @param string $state Full state name or state code. + * @param string $country Two-letter country code. + * + * @return string Normalized state or original state input value. + */ + public function get_normalized_state_from_pr_states( $state, $country ) { + // Include Payment Request API State list for compatibility with WC countries/states. + include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-payment-request-button-states.php'; + $pr_states = WC_Stripe_Payment_Request_Button_States::STATES; + + if ( ! isset( $pr_states[ $country ] ) ) { + return $state; + } + + foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) { + $sanitized_state_string = $this->sanitize_string( $state ); + // Checks if input state matches with Payment Request state code (0), name (1) or localName (2). + if ( + ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) || + ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) || + ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) ) + ) { + return $wc_state_abbr; + } + } + + return $state; + } + + /** + * Get normalized state from WooCommerce list of translated states. + * + * @param string $state Full state name or state code. + * @param string $country Two-letter country code. + * + * @return string Normalized state or original state input value. + */ + public function get_normalized_state_from_wc_states( $state, $country ) { + $wc_states = WC()->countries->get_states( $country ); + + if ( is_array( $wc_states ) ) { + foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) { + if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) { + return $wc_state_abbr; + } + } + } + + return $state; + } + + /** + * Gets the normalized state/county field because in some + * cases, the state/county field is formatted differently from + * what WC is expecting and throws an error. An example + * for Ireland, the county dropdown in Chrome shows "Co. Clare" format. + * + * @param string $state Full state name or an already normalized abbreviation. + * @param string $country Two-letter country code. + * + * @return string Normalized state abbreviation. + */ + public function get_normalized_state( $state, $country ) { + // If it's empty or already normalized, skip. + if ( ! $state || $this->is_normalized_state( $state, $country ) ) { + return $state; + } + + // Try to match state from the Payment Request API list of states. + $state = $this->get_normalized_state_from_pr_states( $state, $country ); + + // If it's normalized, return. + if ( $this->is_normalized_state( $state, $country ) ) { + return $state; + } + + // If the above doesn't work, fallback to matching against the list of translated + // states from WooCommerce. + return $this->get_normalized_state_from_wc_states( $state, $country ); + } + + /** + * The express checkout API provides its own validation for the address form. + * For some countries, it might not provide a state field, so we need to return a more descriptive + * error message, indicating that the express checkout button is not supported for that country. + */ + public function validate_state() { + $wc_checkout = WC_Checkout::instance(); + $posted_data = $wc_checkout->get_posted_data(); + $checkout_fields = $wc_checkout->get_checkout_fields(); + $countries = WC()->countries->get_countries(); + + $is_supported = true; + // Checks if billing state is missing and is required. + if ( ! empty( $checkout_fields['billing']['billing_state']['required'] ) && '' === $posted_data['billing_state'] ) { + $is_supported = false; + } + + // Checks if shipping state is missing and is required. + if ( WC()->cart->needs_shipping_address() && ! empty( $checkout_fields['shipping']['shipping_state']['required'] ) && '' === $posted_data['shipping_state'] ) { + $is_supported = false; + } + + if ( ! $is_supported ) { + wc_add_notice( + sprintf( + /* translators: 1) country. */ + __( 'The Express Checkout button is not supported in %1$s because some required fields couldn\'t be verified. Please proceed to the checkout page and try again.', 'woocommerce-gateway-stripe' ), + isset( $countries[ $posted_data['billing_country'] ] ) ? $countries[ $posted_data['billing_country'] ] : $posted_data['billing_country'] + ), + 'error' + ); + } + } + + /** + * Calculate and set shipping method. + * + * @param array $address Shipping address. + */ + protected function calculate_shipping( $address = [] ) { + $country = $address['country']; + $state = $address['state']; + $postcode = $address['postcode']; + $city = $address['city']; + $address_1 = $address['address']; + $address_2 = $address['address_2']; + + // Normalizes state to calculate shipping zones. + $state = $this->get_normalized_state( $state, $country ); + + // Normalizes postal code in case of redacted data from Apple Pay. + $postcode = $this->get_normalized_postal_code( $postcode, $country ); + + WC()->shipping->reset_shipping(); + + if ( $postcode && WC_Validation::is_postcode( $postcode, $country ) ) { + $postcode = wc_format_postcode( $postcode, $country ); + } + + if ( $country ) { + WC()->customer->set_location( $country, $state, $postcode, $city ); + WC()->customer->set_shipping_location( $country, $state, $postcode, $city ); + } else { + WC()->customer->set_billing_address_to_base(); + WC()->customer->set_shipping_address_to_base(); + } + + WC()->customer->set_calculated_shipping( true ); + WC()->customer->save(); + + $packages = []; + + $packages[0]['contents'] = WC()->cart->get_cart(); + $packages[0]['contents_cost'] = 0; + $packages[0]['applied_coupons'] = WC()->cart->applied_coupons; + $packages[0]['user']['ID'] = get_current_user_id(); + $packages[0]['destination']['country'] = $country; + $packages[0]['destination']['state'] = $state; + $packages[0]['destination']['postcode'] = $postcode; + $packages[0]['destination']['city'] = $city; + $packages[0]['destination']['address'] = $address_1; + $packages[0]['destination']['address_2'] = $address_2; + + foreach ( WC()->cart->get_cart() as $item ) { + if ( $item['data']->needs_shipping() ) { + if ( isset( $item['line_total'] ) ) { + $packages[0]['contents_cost'] += $item['line_total']; + } + } + } + + $packages = apply_filters( 'woocommerce_cart_shipping_packages', $packages ); + + WC()->shipping->calculate_shipping( $packages ); + } + + /** + * The settings for the `button` attribute. + * + * @return array + */ + public function get_button_settings() { + $button_type = $this->get_button_type(); + return [ + 'type' => $button_type, + 'theme' => $this->get_button_theme(), + 'height' => $this->get_button_height(), + // Default format is en_US. + 'locale' => apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) ), + ]; + } + + /** + * Builds the shippings methods to pass to express checkout elements. + */ + protected function build_shipping_methods( $shipping_methods ) { + if ( empty( $shipping_methods ) ) { + return []; + } + + $shipping = []; + + foreach ( $shipping_methods as $method ) { + $shipping[] = [ + 'id' => $method['id'], + 'label' => $method['label'], + 'detail' => '', + 'amount' => WC_Stripe_Helper::get_stripe_amount( $method['amount']['value'] ), + ]; + } + + return $shipping; + } + + /** + * Builds the line items to pass to express checkout elements. + */ + protected function build_display_items( $itemized_display_items = false ) { + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + $items = []; + $lines = []; + $subtotal = 0; + $discounts = 0; + $display_items = ! apply_filters( 'wc_stripe_payment_request_hide_itemization', true ) || $itemized_display_items; + + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $subtotal += $cart_item['line_subtotal']; + $amount = $cart_item['line_subtotal']; + $quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : ''; + $product_name = $cart_item['data']->get_name(); + + $lines[] = [ + 'label' => $product_name . $quantity_label, + 'amount' => WC_Stripe_Helper::get_stripe_amount( $amount ), + ]; + } + + if ( $display_items ) { + $items = array_merge( $items, $lines ); + } else { + // Default show only subtotal instead of itemization. + + $items[] = [ + 'label' => 'Subtotal', + 'amount' => WC_Stripe_Helper::get_stripe_amount( $subtotal ), + ]; + } + + $applied_coupons = array_values( WC()->cart->get_coupon_discount_totals() ); + + foreach ( $applied_coupons as $amount ) { + $discounts += (float) $amount; + } + + $discounts = wc_format_decimal( $discounts, WC()->cart->dp ); + $tax = wc_format_decimal( WC()->cart->tax_total + WC()->cart->shipping_tax_total, WC()->cart->dp ); + $shipping = wc_format_decimal( WC()->cart->shipping_total, WC()->cart->dp ); + $items_total = wc_format_decimal( WC()->cart->cart_contents_total, WC()->cart->dp ) + $discounts; + $order_total = WC()->cart->get_total( false ); + + if ( wc_tax_enabled() ) { + $items[] = [ + 'label' => esc_html( __( 'Tax', 'woocommerce-gateway-stripe' ) ), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $tax ), + ]; + } + + if ( WC()->cart->needs_shipping() ) { + $items[] = [ + 'label' => esc_html( __( 'Shipping', 'woocommerce-gateway-stripe' ) ), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $shipping ), + ]; + } + + if ( WC()->cart->has_discount() ) { + $items[] = [ + 'label' => esc_html( __( 'Discount', 'woocommerce-gateway-stripe' ) ), + 'amount' => WC_Stripe_Helper::get_stripe_amount( $discounts ), + ]; + } + + $cart_fees = WC()->cart->get_fees(); + + // Include fees and taxes as display items. + foreach ( $cart_fees as $key => $fee ) { + $items[] = [ + 'label' => $fee->name, + 'amount' => WC_Stripe_Helper::get_stripe_amount( $fee->amount ), + ]; + } + + return [ + 'displayItems' => $items, + 'total' => [ + 'label' => $this->total_label, + 'amount' => max( 0, apply_filters( 'woocommerce_stripe_calculated_total', WC_Stripe_Helper::get_stripe_amount( $order_total ), $order_total, WC()->cart ) ), + 'pending' => false, + ], + ]; + } + + /** + * Settings array for the user authentication dialog and redirection. + * + * @return array + */ + public function get_login_confirmation_settings() { + if ( is_user_logged_in() || ! $this->is_authentication_required() ) { + return false; + } + + /* translators: The text encapsulated in `**` can be replaced with "Apple Pay" or "Google Pay". Please translate this text, but don't remove the `**`. */ + $message = __( 'To complete your transaction with **the selected payment method**, you must log in or create an account with our site.', 'woocommerce-gateway-stripe' ); + $redirect_url = add_query_arg( + [ + '_wpnonce' => wp_create_nonce( 'wc-stripe-set-redirect-url' ), + 'wc_stripe_express_checkout_redirect_url' => rawurlencode( home_url( add_query_arg( [] ) ) ), // Current URL to redirect to after login. + ], + home_url() + ); + + return [ + 'message' => $message, + 'redirect_url' => wp_sanitize_redirect( esc_url_raw( $redirect_url ) ), + ]; + } + + /** + * Pages where the express checkout buttons should be displayed. + * + * @return array + */ + public function get_button_locations() { + // If the locations have not been set return the default setting. + if ( ! isset( $this->stripe_settings['payment_request_button_locations'] ) ) { + return [ 'product', 'cart' ]; + } + + // If all locations are removed through the settings UI the location config will be set to + // an empty string "". If that's the case (and if the settings are not an array for any + // other reason) we should return an empty array. + if ( ! is_array( $this->stripe_settings['payment_request_button_locations'] ) ) { + return []; + } + + return $this->stripe_settings['payment_request_button_locations']; + } + + /** + * Returns whether Stripe express checkout element is enabled. + * + * This option defines whether Apple Pay and Google Pay buttons are enabled. + * + * @return boolean + */ + private function is_express_checkout_enabled() { + return isset( $this->stripe_settings['payment_request'] ) && 'yes' === $this->stripe_settings['payment_request']; + } + + /** + * Restores the shipping methods previously chosen for each recurring cart after shipping was reset and recalculated + * during the express checkout get_shipping_options flow. + * + * When the cart contains multiple subscriptions with different billing periods, customers are able to select different shipping + * methods for each subscription, however, this is not supported when purchasing with Apple Pay and Google Pay as it's + * only concerned about handling the initial purchase. + * + * In order to avoid Woo Subscriptions's `WC_Subscriptions_Cart::validate_recurring_shipping_methods` throwing an error, we need to restore + * the previously chosen shipping methods for each recurring cart. + * + * This function needs to be called after `WC()->cart->calculate_totals()` is run, otherwise `WC()->cart->recurring_carts` won't exist yet. + * + * @param array $previous_chosen_methods The previously chosen shipping methods. + */ + private function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) { + if ( empty( WC()->cart->recurring_carts ) || ! method_exists( 'WC_Subscriptions_Cart', 'get_recurring_shipping_package_key' ) ) { + return; + } + + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); + + foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) { + foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_index => $recurring_cart_package ) { + if ( class_exists( 'WC_Subscriptions_Cart' ) ) { + $package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $recurring_cart_key, $recurring_cart_package_index ); + + // If the recurring cart package key is found in the previous chosen methods, but not in the current chosen methods, restore it. + if ( isset( $previous_chosen_methods[ $package_key ] ) && ! isset( $chosen_shipping_methods[ $package_key ] ) ) { + $chosen_shipping_methods[ $package_key ] = $previous_chosen_methods[ $package_key ]; + } + } + } + } + + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } +} From e3f68d393bbc8a3a412a61bf7dad6ea9388cfc33 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Wed, 11 Sep 2024 00:38:50 +0600 Subject: [PATCH 012/120] include and initialize express checkout classes --- woocommerce-gateway-stripe.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/woocommerce-gateway-stripe.php b/woocommerce-gateway-stripe.php index e4c4a1ab2..f165cb0b8 100644 --- a/woocommerce-gateway-stripe.php +++ b/woocommerce-gateway-stripe.php @@ -134,7 +134,7 @@ public static function get_instance() { /** * Stripe Express Checkout configurations. * - * @var WC_Stripe_Express_Checkout + * @var WC_Stripe_Express_Checkout_Element */ public $express_checkout_configuration; @@ -245,6 +245,8 @@ public function init() { require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-gateway-stripe-oxxo.php'; require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-stripe-payment-request.php'; require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-stripe-express-checkout-element.php'; + require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-stripe-express-checkout-helper.php'; + require_once dirname( __FILE__ ) . '/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php'; require_once dirname( __FILE__ ) . '/includes/compat/class-wc-stripe-woo-compat-utils.php'; require_once dirname( __FILE__ ) . '/includes/connect/class-wc-stripe-connect.php'; require_once dirname( __FILE__ ) . '/includes/connect/class-wc-stripe-connect-api.php'; @@ -261,9 +263,15 @@ public function init() { $this->api = new WC_Stripe_Connect_API(); $this->connect = new WC_Stripe_Connect( $this->api ); $this->payment_request_configuration = new WC_Stripe_Payment_Request(); - $this->express_checkout_configuration = new WC_Stripe_Express_Checkout_Element(); $this->account = new WC_Stripe_Account( $this->connect, 'WC_Stripe_API' ); + // Express checkout configurations. + $express_checkout_helper = new WC_Stripe_Express_Checkout_Helper(); + $express_checkout_ajax_handler = new WC_Stripe_Express_Checkout_Ajax_Handler( $express_checkout_helper ); + $this->express_checkout_configuration = new WC_Stripe_Express_Checkout_Element( $express_checkout_ajax_handler, $express_checkout_helper); + $this->express_checkout_configuration->init(); + + $intent_controller = new WC_Stripe_Intent_Controller(); $intent_controller->init_hooks(); From 55e661c48f84783c9099db1cb9c8157dc5d04442 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Wed, 11 Sep 2024 14:01:42 +0600 Subject: [PATCH 013/120] make functions public in the helper class --- .../class-wc-stripe-express-checkout-helper.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-helper.php b/includes/payment-methods/class-wc-stripe-express-checkout-helper.php index a641b62d4..d4f01ec35 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-helper.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-helper.php @@ -435,7 +435,7 @@ public function get_product() { * * @return boolean True if the current page is supported, false otherwise. */ - private function is_page_supported() { + public function is_page_supported() { return $this->is_product() || WC_Stripe_Helper::has_cart_or_checkout_on_current_page() || is_wc_endpoint_url( 'order-pay' ); @@ -559,7 +559,7 @@ public function should_show_ece_on_product_pages() { * * @return boolean True if the provided product is supported, false otherwise. */ - private function is_product_supported( $product ) { + public function is_product_supported( $product ) { if ( ! is_object( $product ) || ! in_array( $product->get_type(), $this->supported_product_types() ) ) { return false; } @@ -1141,7 +1141,7 @@ public function get_button_locations() { * * @return boolean */ - private function is_express_checkout_enabled() { + public function is_express_checkout_enabled() { return isset( $this->stripe_settings['payment_request'] ) && 'yes' === $this->stripe_settings['payment_request']; } @@ -1160,7 +1160,7 @@ private function is_express_checkout_enabled() { * * @param array $previous_chosen_methods The previously chosen shipping methods. */ - private function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) { + public function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) { if ( empty( WC()->cart->recurring_carts ) || ! method_exists( 'WC_Subscriptions_Cart', 'get_recurring_shipping_package_key' ) ) { return; } From 08c3205eb12a62c1c5bac2d99eeba8c4f1458865 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Wed, 11 Sep 2024 14:02:11 +0600 Subject: [PATCH 014/120] fix lint issue --- client/entrypoints/express-checkout/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/entrypoints/express-checkout/index.js b/client/entrypoints/express-checkout/index.js index b0a327bb3..f69a9256f 100644 --- a/client/entrypoints/express-checkout/index.js +++ b/client/entrypoints/express-checkout/index.js @@ -1,3 +1 @@ // express checkout element integration for shortcode goes here. - -console.log('Express Checkout entrypoint'); From 1f4234792f8f982ea2da69f04e9045a0e7d03623 Mon Sep 17 00:00:00 2001 From: Mayisha Date: Wed, 11 Sep 2024 14:29:20 +0600 Subject: [PATCH 015/120] use correct global variable --- client/blocks/express-checkout/express-checkout.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/blocks/express-checkout/express-checkout.js b/client/blocks/express-checkout/express-checkout.js index 345758acc..297fda109 100644 --- a/client/blocks/express-checkout/express-checkout.js +++ b/client/blocks/express-checkout/express-checkout.js @@ -1,4 +1,4 @@ -/* global wc_stripe_payment_request_params */ +/* global wc_stripe_express_checkout_params */ import React from 'react'; import { Elements, ExpressCheckoutElement } from '@stripe/react-stripe-js'; @@ -13,8 +13,8 @@ export const ExpressCheckout = ( props ) => { const buttonOptions = { buttonType: { - googlePay: wc_stripe_payment_request_params.button.type, - applePay: wc_stripe_payment_request_params.button.type, + googlePay: wc_stripe_express_checkout_params.button.type, + applePay: wc_stripe_express_checkout_params.button.type, }, }; From 632b63ccef0fcd0f93dfee95753540239b81c95d Mon Sep 17 00:00:00 2001 From: Mayisha Date: Wed, 11 Sep 2024 14:40:10 +0600 Subject: [PATCH 016/120] fix php lint issues --- .../class-wc-stripe-express-checkout-ajax-handler.php | 2 +- woocommerce-gateway-stripe.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php b/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php index 77cbe34e8..ade4b0132 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php @@ -4,7 +4,7 @@ } /** - * class WC_Stripe_Express_Checkout_Ajax_Handler class. + * WC_Stripe_Express_Checkout_Ajax_Handler class. */ class WC_Stripe_Express_Checkout_Ajax_Handler { /** diff --git a/woocommerce-gateway-stripe.php b/woocommerce-gateway-stripe.php index f165cb0b8..3d4f42f1f 100644 --- a/woocommerce-gateway-stripe.php +++ b/woocommerce-gateway-stripe.php @@ -268,9 +268,8 @@ public function init() { // Express checkout configurations. $express_checkout_helper = new WC_Stripe_Express_Checkout_Helper(); $express_checkout_ajax_handler = new WC_Stripe_Express_Checkout_Ajax_Handler( $express_checkout_helper ); - $this->express_checkout_configuration = new WC_Stripe_Express_Checkout_Element( $express_checkout_ajax_handler, $express_checkout_helper); + $this->express_checkout_configuration = new WC_Stripe_Express_Checkout_Element( $express_checkout_ajax_handler, $express_checkout_helper ); $this->express_checkout_configuration->init(); - $intent_controller = new WC_Stripe_Intent_Controller(); $intent_controller->init_hooks(); From 1be492169a54a2ebcedc22f0c4e0152e239c5640 Mon Sep 17 00:00:00 2001 From: Wesley Rosa Date: Fri, 13 Sep 2024 17:00:31 -0300 Subject: [PATCH 017/120] Integrating ECE to shortcode checkout --- client/entrypoints/express-checkout/index.js | 58 ++++++++++++++++++- includes/class-wc-stripe-feature-flags.php | 3 +- ...ass-wc-stripe-express-checkout-element.php | 4 +- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/client/entrypoints/express-checkout/index.js b/client/entrypoints/express-checkout/index.js index f69a9256f..160810023 100644 --- a/client/entrypoints/express-checkout/index.js +++ b/client/entrypoints/express-checkout/index.js @@ -1 +1,57 @@ -// express checkout element integration for shortcode goes here. +/* global wc_stripe_express_checkout_params */ + +import jQuery from 'jquery'; +import WCStripeAPI from '../../api'; +import { getStripeServerData } from 'wcstripe/stripe-utils'; + +jQuery( function () { + const api = new WCStripeAPI( + getStripeServerData(), + // A promise-based interface to jQuery.post. + ( url, args ) => { + return new Promise( ( resolve, reject ) => { + jQuery.post( url, args ).then( resolve ).fail( reject ); + } ); + } + ); + const appearance = { + /* appearance */ + }; + const buttonHeight = Math.min( + Math.max( + parseInt( + wc_stripe_express_checkout_params.button.height ?? '48', + 10 + ), + 40 + ), + 55 + ); + const options = { + paymentMethods: { + applePay: 'always', + googlePay: 'always', + link: 'never', + paypal: 'never', + amazonPay: 'never', + }, + layout: { overflow: 'never' }, + buttonType: { + googlePay: wc_stripe_express_checkout_params.button.type, + applePay: wc_stripe_express_checkout_params.button.type, + }, + // Allowed height must be 40px to 55px. + buttonHeight, + }; + const elements = api.getStripe().elements( { + mode: 'payment', + amount: 1099, + currency: 'usd', + appearance, + } ); + const expressCheckoutElement = elements.create( + 'expressCheckout', + options + ); + expressCheckoutElement.mount( '#wc-stripe-express-checkout-button' ); +} ); diff --git a/includes/class-wc-stripe-feature-flags.php b/includes/class-wc-stripe-feature-flags.php index 8215962c5..2a8520c53 100644 --- a/includes/class-wc-stripe-feature-flags.php +++ b/includes/class-wc-stripe-feature-flags.php @@ -14,7 +14,8 @@ class WC_Stripe_Feature_Flags { * @return bool */ public static function is_stripe_ece_enabled() { - return 'yes' === get_option( self::ECE_FEATURE_FLAG_NAME, 'no' ); + // @todo revert this + return true; } /** diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-element.php b/includes/payment-methods/class-wc-stripe-express-checkout-element.php index 7e41cf8cf..6fd796eb4 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-element.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php @@ -324,7 +324,7 @@ public function display_express_checkout_button_html() { } ?> -