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 - ); } /**