diff --git a/changelog.txt b/changelog.txt
index 232e5078a..ffbbe9268 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,6 +1,7 @@
*** Changelog ***
= 8.9.0 - xxxx-xx-xx =
+* Fix - Display a notice if taxes vary by customer's billing address when checking out using the Stripe Express Checkout Element.
* Tweak - Makes the new Stripe Express Checkout Element enabled by default.
* Dev - Add multiple unit tests for the Stripe Express Checkout Element implementation (for both frontend and backend).
* Fix - Check if taxes are enabled when applying ECE tax compatibility check.
@@ -14,6 +15,7 @@
* Tweak - Add error logging in ECE critical Ajax requests.
* Add - Add support for Stripe Link payments via the new Stripe Checkout Element on the block cart and block checkout pages.
* Add - Add support for Stripe Link payments via the new Stripe Checkout Element on the product, cart, checkout and pay for order pages.
+* Tweak - Remove the subscription order notes added each time a source wasn't migrated.
* Fix - Prevent marking renewal orders as processing/completed multiple times due to handling the Stripe webhook in parallel.
= 8.8.1 - 2024-10-28 =
diff --git a/client/blocks/express-checkout/hooks.js b/client/blocks/express-checkout/hooks.js
index 20a7803a5..35c85615f 100644
--- a/client/blocks/express-checkout/hooks.js
+++ b/client/blocks/express-checkout/hooks.js
@@ -1,4 +1,5 @@
import { useCallback } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
import { useStripe, useElements } from '@stripe/react-stripe-js';
import {
onAbortPaymentHandler,
@@ -8,6 +9,8 @@ import {
onConfirmHandler,
} from 'wcstripe/express-checkout/event-handler';
import {
+ displayExpressCheckoutNotice,
+ expressCheckoutNoticeDelay,
getExpressCheckoutButtonStyleSettings,
getExpressCheckoutData,
normalizeLineItems,
@@ -43,7 +46,7 @@ export const useExpressCheckout = ( {
};
const onButtonClick = useCallback(
- ( event ) => {
+ async ( event ) => {
const getShippingRates = () => {
// shippingData.shippingRates[ 0 ].shipping_rates will be non-empty
// only when the express checkout element's default shipping address
@@ -82,6 +85,20 @@ export const useExpressCheckout = ( {
// Click event from WC Blocks.
onClick();
+
+ if ( getExpressCheckoutData( 'taxes_based_on_billing' ) ) {
+ displayExpressCheckoutNotice(
+ __(
+ 'Final taxes charged can differ based on your actual billing address when using Express Checkout buttons (Link, Google Pay or Apple Pay).',
+ 'woocommerce-gateway-stripe'
+ ),
+ 'info',
+ [ 'ece-taxes-info' ]
+ );
+ // Wait for the notice to be displayed before proceeding.
+ await expressCheckoutNoticeDelay();
+ }
+
// Global click event handler to ECE.
onClickHandler( event );
event.resolve( options );
diff --git a/client/data/constants.js b/client/data/constants.js
index 4eb9f4350..6f51eac9e 100644
--- a/client/data/constants.js
+++ b/client/data/constants.js
@@ -1,2 +1,20 @@
export const NAMESPACE = '/wc/v3/wc_stripe';
export const STORE_NAME = 'wc/stripe';
+
+/**
+ * The amount threshold for displaying the notice.
+ *
+ * @type {number} The threshold amount.
+ */
+export const CASH_APP_NOTICE_AMOUNT_THRESHOLD = 200000;
+
+/**
+ * Wait time in ms for a notice to be displayed in ECE before proceeding with the checkout process.
+ *
+ * Reasons for this value:
+ * - We cannot display an alert message because it blocks the default ECE process
+ * - The delay cannot be higher than 1s due to Stripe JS limitations (it times out after 1s)
+ *
+ * @type {number} The delay in milliseconds.
+ */
+export const EXPRESS_CHECKOUT_NOTICE_DELAY = 700;
diff --git a/client/entrypoints/express-checkout/index.js b/client/entrypoints/express-checkout/index.js
index a114e57fb..a5d36aaaf 100644
--- a/client/entrypoints/express-checkout/index.js
+++ b/client/entrypoints/express-checkout/index.js
@@ -4,7 +4,9 @@ import { debounce } from 'lodash';
import jQuery from 'jquery';
import WCStripeAPI from '../../api';
import {
+ displayExpressCheckoutNotice,
displayLoginConfirmation,
+ expressCheckoutNoticeDelay,
getExpressCheckoutButtonAppearance,
getExpressCheckoutButtonStyleSettings,
getExpressCheckoutData,
@@ -147,13 +149,26 @@ jQuery( function ( $ ) {
);
} );
- eceButton.on( 'click', function ( event ) {
+ eceButton.on( 'click', async function ( event ) {
// If login is required for checkout, display redirect confirmation dialog.
if ( getExpressCheckoutData( 'login_confirmation' ) ) {
displayLoginConfirmation( event.expressPaymentType );
return;
}
+ if ( getExpressCheckoutData( 'taxes_based_on_billing' ) ) {
+ displayExpressCheckoutNotice(
+ __(
+ 'Final taxes charged can differ based on your actual billing address when using Express Checkout buttons (Link, Google Pay or Apple Pay).',
+ 'woocommerce-gateway-stripe'
+ ),
+ 'info',
+ [ 'ece-taxes-info' ]
+ );
+ // Wait for the notice to be displayed before proceeding.
+ await expressCheckoutNoticeDelay();
+ }
+
if ( getExpressCheckoutData( 'is_product_page' ) ) {
const addToCartButton = $( '.single_add_to_cart_button' );
@@ -456,24 +471,7 @@ jQuery( function ( $ ) {
payment.paymentFailed( { reason: 'fail' } );
onAbortPaymentHandler( payment, message );
- $( '.woocommerce-error' ).remove();
-
- const $container = $( '.woocommerce-notices-wrapper' ).first();
-
- if ( $container.length ) {
- $container.append(
- $( '
' ).text( message )
- );
-
- $( 'html, body' ).animate(
- {
- scrollTop: $container
- .find( '.woocommerce-error' )
- .offset().top,
- },
- 600
- );
- }
+ displayExpressCheckoutNotice( message, 'error' );
},
attachProductPageEventListeners: ( elements ) => {
diff --git a/client/express-checkout/utils/__tests__/index.test.js b/client/express-checkout/utils/__tests__/index.test.js
index 8a307e458..bb119fb34 100644
--- a/client/express-checkout/utils/__tests__/index.test.js
+++ b/client/express-checkout/utils/__tests__/index.test.js
@@ -1,7 +1,12 @@
/**
* Internal dependencies
*/
-import { getErrorMessageFromNotice, getExpressCheckoutData } from '..';
+import { screen, render } from '@testing-library/react';
+import {
+ displayExpressCheckoutNotice,
+ getErrorMessageFromNotice,
+ getExpressCheckoutData,
+} from '..';
describe( 'Express checkout utils', () => {
test( 'getExpressCheckoutData returns null for missing option', () => {
@@ -37,4 +42,45 @@ describe( 'Express checkout utils', () => {
'Error: Payment failed.alert("hello")'
);
} );
+
+ describe( 'displayExpressCheckoutNotice', () => {
+ afterEach( () => {
+ document.getElementsByTagName( 'body' )[ 0 ].innerHTML = '';
+ } );
+
+ const additionalClasses = [ 'class-2', 'class-3' ];
+ const createWrapper = () => {
+ const wrapper = document.createElement( 'div' );
+ wrapper.classList.add( 'woocommerce-notices-wrapper' );
+ document.body.appendChild( wrapper );
+ };
+
+ test( 'with info', async () => {
+ function App() {
+ createWrapper();
+ displayExpressCheckoutNotice(
+ 'Test message',
+ 'info',
+ additionalClasses
+ );
+ return ;
+ }
+ render( );
+ expect( screen.queryByRole( 'note' ) ).toBeInTheDocument();
+ } );
+
+ test( 'with error', () => {
+ function App() {
+ createWrapper();
+ displayExpressCheckoutNotice(
+ 'Test message',
+ 'error',
+ additionalClasses
+ );
+ return ;
+ }
+ render( );
+ expect( screen.queryByRole( 'note' ) ).toBeInTheDocument();
+ } );
+ } );
} );
diff --git a/client/express-checkout/utils/index.js b/client/express-checkout/utils/index.js
index 6abac49d8..1291af832 100644
--- a/client/express-checkout/utils/index.js
+++ b/client/express-checkout/utils/index.js
@@ -1,6 +1,8 @@
/* global wc_stripe_express_checkout_params */
+import jQuery from 'jquery';
import { isLinkEnabled, getPaymentMethodTypes } from 'wcstripe/stripe-utils';
import { getBlocksConfiguration } from 'wcstripe/blocks/utils';
+import { EXPRESS_CHECKOUT_NOTICE_DELAY } from 'wcstripe/data/constants';
export * from './normalize';
@@ -306,3 +308,62 @@ export const getPaymentMethodTypesForExpressMethod = ( paymentMethodType ) => {
return paymentMethodTypes;
};
+
+/**
+ * Display a notice on the checkout page (for Express Checkout Element).
+ *
+ * @param {string} message The message to display.
+ * @param {string} type The type of notice.
+ * @param {Array} additionalClasses Additional classes to add to the notice.
+ */
+export const displayExpressCheckoutNotice = (
+ message,
+ type,
+ additionalClasses
+) => {
+ const isBlockCheckout = getExpressCheckoutData( 'has_block' );
+ const mainNoticeClass = `woocommerce-${ type }`;
+ let classNames = [ mainNoticeClass ];
+ if ( additionalClasses ) {
+ classNames = classNames.concat( additionalClasses );
+ }
+
+ // Remove any existing notices.
+ jQuery( '.' + classNames.join( '.' ) ).remove();
+
+ const containerClass = isBlockCheckout
+ ? 'wc-block-components-main'
+ : 'woocommerce-notices-wrapper';
+ const $container = jQuery( '.' + containerClass ).first();
+
+ if ( $container.length ) {
+ const note = jQuery(
+ ``
+ ).text( message );
+ if ( isBlockCheckout ) {
+ $container.prepend( note );
+ } else {
+ $container.append( note );
+ }
+
+ // Scroll to notices.
+ jQuery( 'html, body' ).animate(
+ {
+ scrollTop: $container.find( `.${ mainNoticeClass }` ).offset()
+ .top,
+ },
+ 600
+ );
+ }
+};
+
+/**
+ * Delay for a short period of time before proceeding with the checkout process.
+ *
+ * @return {Promise} A promise that resolves after the delay.
+ */
+export const expressCheckoutNoticeDelay = async () => {
+ await new Promise( ( resolve ) =>
+ setTimeout( resolve, EXPRESS_CHECKOUT_NOTICE_DELAY )
+ );
+};
diff --git a/client/stripe-utils/cash-app-limit-notice-handler.js b/client/stripe-utils/cash-app-limit-notice-handler.js
index 34ec2d536..e2b5cad76 100644
--- a/client/stripe-utils/cash-app-limit-notice-handler.js
+++ b/client/stripe-utils/cash-app-limit-notice-handler.js
@@ -1,8 +1,6 @@
import { __ } from '@wordpress/i18n';
import { callWhenElementIsAvailable } from 'wcstripe/blocks/upe/call-when-element-is-available';
-
-/** The amount threshold for displaying the notice. */
-export const CASH_APP_NOTICE_AMOUNT_THRESHOLD = 200000;
+import { CASH_APP_NOTICE_AMOUNT_THRESHOLD } from 'wcstripe/data/constants';
/** The class name for the limit notice element. */
const LIMIT_NOTICE_CLASSNAME = 'wc-block-checkout__payment-method-limit-notice';
diff --git a/includes/compat/class-wc-stripe-subscriptions-legacy-sepa-token-update.php b/includes/compat/class-wc-stripe-subscriptions-legacy-sepa-token-update.php
index b294942ae..c6cc78ad0 100644
--- a/includes/compat/class-wc-stripe-subscriptions-legacy-sepa-token-update.php
+++ b/includes/compat/class-wc-stripe-subscriptions-legacy-sepa-token-update.php
@@ -70,14 +70,10 @@ public function maybe_update_subscription_legacy_payment_method( $subscription_i
public function maybe_update_subscription_source( WC_Subscription $subscription ) {
try {
$this->set_subscription_updated_payment_method( $subscription );
-
- $order_note = __( 'Stripe Gateway: The payment method used for renewals was updated from Sources to PaymentMethods.', 'woocommerce-gateway-stripe' );
+ $subscription->add_order_note( __( 'Stripe Gateway: The payment method used for renewals was updated from Sources to PaymentMethods.', 'woocommerce-gateway-stripe' ) );
} catch ( \Exception $e ) {
- /* translators: Reason why the subscription payment method wasn't updated */
- $order_note = sprintf( __( 'Stripe Gateway: A Source is used for renewals but could not be updated to PaymentMethods. Reason: %s', 'woocommerce-gateway-stripe' ), $e->getMessage() );
+ WC_Stripe_Logger::log( $e->getMessage() );
}
-
- $subscription->add_order_note( $order_note );
}
/**
@@ -131,6 +127,11 @@ private function set_subscription_updated_payment_method( WC_Subscription $subsc
// Retrieve the source object from the API.
$source_object = WC_Stripe_API::get_payment_method( $source_id );
+ // Bail out, if the source object isn't expected to be migrated. eg Card sources are not migrated.
+ if ( isset( $source_object->type ) && 'card' === $source_object->type ) {
+ throw new \Exception( sprintf( 'Skipping migration of Source for subscription #%d. Source is a card.', $subscription->get_id() ) );
+ }
+
// Bail out if the src_ hasn't been migrated to pm_ yet.
if ( ! isset( $source_object->metadata->migrated_payment_method ) ) {
throw new \Exception( sprintf( 'The Source has not been migrated to PaymentMethods on the Stripe account.', $subscription->get_id() ) );
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 2bb841cc5..7b0745ae6 100644
--- a/includes/payment-methods/class-wc-stripe-express-checkout-element.php
+++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php
@@ -169,15 +169,15 @@ public function get_login_redirect_url( $redirect ) {
*/
public function javascript_params() {
return [
- 'ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
- 'stripe' => [
+ 'ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
+ 'stripe' => [
'publishable_key' => 'yes' === $this->stripe_settings['testmode'] ? $this->stripe_settings['test_publishable_key'] : $this->stripe_settings['publishable_key'],
'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->express_checkout_helper->is_express_checkout_enabled(),
],
- 'nonce' => [
+ 'nonce' => [
'payment' => wp_create_nonce( 'wc-stripe-express-checkout' ),
'shipping' => wp_create_nonce( 'wc-stripe-express-checkout-shipping' ),
'get_cart_details' => wp_create_nonce( 'wc-stripe-get-cart-details' ),
@@ -189,20 +189,21 @@ public function javascript_params() {
'clear_cart' => wp_create_nonce( 'wc-stripe-clear-cart' ),
'pay_for_order' => wp_create_nonce( 'wc-stripe-pay-for-order' ),
],
- 'i18n' => [
+ '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' => $this->express_checkout_helper->get_checkout_data(),
- 'button' => $this->express_checkout_helper->get_button_settings(),
- 'is_pay_for_order' => $this->express_checkout_helper->is_pay_for_order_page(),
- 'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ),
- 'login_confirmation' => $this->express_checkout_helper->get_login_confirmation_settings(),
- 'is_product_page' => $this->express_checkout_helper->is_product(),
- 'is_checkout_page' => $this->express_checkout_helper->is_checkout(),
- 'product' => $this->express_checkout_helper->get_product_data(),
- 'is_cart_page' => is_cart(),
+ 'checkout' => $this->express_checkout_helper->get_checkout_data(),
+ 'button' => $this->express_checkout_helper->get_button_settings(),
+ 'is_pay_for_order' => $this->express_checkout_helper->is_pay_for_order_page(),
+ 'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ),
+ 'login_confirmation' => $this->express_checkout_helper->get_login_confirmation_settings(),
+ 'is_product_page' => $this->express_checkout_helper->is_product(),
+ 'is_checkout_page' => $this->express_checkout_helper->is_checkout(),
+ 'product' => $this->express_checkout_helper->get_product_data(),
+ 'is_cart_page' => is_cart(),
+ 'taxes_based_on_billing' => wc_tax_enabled() && get_option( 'woocommerce_tax_based_on' ) === 'billing',
];
}
diff --git a/readme.txt b/readme.txt
index 71e653f29..c8f7228ee 100644
--- a/readme.txt
+++ b/readme.txt
@@ -111,6 +111,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
== Changelog ==
= 8.9.0 - xxxx-xx-xx =
+* Fix - Display a notice if taxes vary by customer's billing address when checking out using the Stripe Express Checkout Element.
* Tweak - Makes the new Stripe Express Checkout Element enabled by default.
* Dev - Add multiple unit tests for the Stripe Express Checkout Element implementation (for both frontend and backend).
* Fix - Check if taxes are enabled when applying ECE tax compatibility check.
@@ -124,6 +125,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
* Tweak - Add error logging in ECE critical Ajax requests.
* Add - Add support for Stripe Link payments via the new Stripe Checkout Element on the block cart and block checkout pages.
* Add - Add support for Stripe Link payments via the new Stripe Checkout Element on the product, cart, checkout and pay for order pages.
+* Tweak - Remove the subscription order notes added each time a source wasn't migrated.
* Fix - Prevent marking renewal orders as processing/completed multiple times due to handling the Stripe webhook in parallel.
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).
diff --git a/tests/phpunit/admin/migrations/test-class-wc-stripe-subscriptions-repairer-legacy-sepa-tokens.php b/tests/phpunit/admin/migrations/test-class-wc-stripe-subscriptions-repairer-legacy-sepa-tokens.php
index 498248b8e..90475e17b 100644
--- a/tests/phpunit/admin/migrations/test-class-wc-stripe-subscriptions-repairer-legacy-sepa-tokens.php
+++ b/tests/phpunit/admin/migrations/test-class-wc-stripe-subscriptions-repairer-legacy-sepa-tokens.php
@@ -328,18 +328,9 @@ function ( $id ) {
$this->updater->maybe_migrate_before_renewal( $subscription_id );
$subscription = new WC_Subscription( $subscription_id );
- $notes = wc_get_order_notes(
- [ 'order_id' => $subscription_id ]
- );
// Confirm the subscription's payment method remains the same.
$this->assertEquals( $pm_id, $subscription->get_meta( self::SOURCE_ID_META_KEY ) );
-
- // Confirm a note is added when the Source wasn't migrated to PaymentMethods.
- $this->assertEquals(
- 'Stripe Gateway: A Source is used for renewals but could not be updated to PaymentMethods. Reason: The subscription is not using a Stripe Source for renewals.',
- $notes[0]->content
- );
}
public function test_maybe_update_subscription_legacy_payment_method_adds_note_when_source_not_migrated() {
@@ -361,18 +352,9 @@ function ( $id ) {
$this->updater->maybe_migrate_before_renewal( $subscription_id );
$subscription = new WC_Subscription( $subscription_id );
- $notes = wc_get_order_notes(
- [ 'order_id' => $subscription_id ]
- );
// Confirm the subscription's payment method remains the same.
$this->assertEquals( $source_id, $subscription->get_meta( self::SOURCE_ID_META_KEY ) );
-
- // Confirm a note is added when the Source wasn't migrated to PaymentMethods.
- $this->assertEquals(
- 'Stripe Gateway: A Source is used for renewals but could not be updated to PaymentMethods. Reason: The Source has not been migrated to PaymentMethods on the Stripe account.',
- $notes[0]->content
- );
}
/**