Skip to content

Commit

Permalink
Merge branch 'develop' into fix/3568-duplicate-order-notes-subscripti…
Browse files Browse the repository at this point in the history
…on-renewals
  • Loading branch information
mattallan authored Nov 5, 2024
2 parents 5f5c714 + b3731e2 commit 86ab175
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 61 deletions.
2 changes: 2 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 =
Expand Down
19 changes: 18 additions & 1 deletion client/blocks/express-checkout/hooks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { useStripe, useElements } from '@stripe/react-stripe-js';
import {
onAbortPaymentHandler,
Expand All @@ -8,6 +9,8 @@ import {
onConfirmHandler,
} from 'wcstripe/express-checkout/event-handler';
import {
displayExpressCheckoutNotice,
expressCheckoutNoticeDelay,
getExpressCheckoutButtonStyleSettings,
getExpressCheckoutData,
normalizeLineItems,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 );
Expand Down
18 changes: 18 additions & 0 deletions client/data/constants.js
Original file line number Diff line number Diff line change
@@ -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;
36 changes: 17 additions & 19 deletions client/entrypoints/express-checkout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { debounce } from 'lodash';
import jQuery from 'jquery';
import WCStripeAPI from '../../api';
import {
displayExpressCheckoutNotice,
displayLoginConfirmation,
expressCheckoutNoticeDelay,
getExpressCheckoutButtonAppearance,
getExpressCheckoutButtonStyleSettings,
getExpressCheckoutData,
Expand Down Expand Up @@ -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' );

Expand Down Expand Up @@ -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(
$( '<div class="woocommerce-error" />' ).text( message )
);

$( 'html, body' ).animate(
{
scrollTop: $container
.find( '.woocommerce-error' )
.offset().top,
},
600
);
}
displayExpressCheckoutNotice( message, 'error' );
},

attachProductPageEventListeners: ( elements ) => {
Expand Down
48 changes: 47 additions & 1 deletion client/express-checkout/utils/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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 <div />;
}
render( <App /> );
expect( screen.queryByRole( 'note' ) ).toBeInTheDocument();
} );

test( 'with error', () => {
function App() {
createWrapper();
displayExpressCheckoutNotice(
'Test message',
'error',
additionalClasses
);
return <div />;
}
render( <App /> );
expect( screen.queryByRole( 'note' ) ).toBeInTheDocument();
} );
} );
} );
61 changes: 61 additions & 0 deletions client/express-checkout/utils/index.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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(
`<div class="${ classNames.join( ' ' ) }" role="note" />`
).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<void>} A promise that resolves after the delay.
*/
export const expressCheckoutNoticeDelay = async () => {
await new Promise( ( resolve ) =>
setTimeout( resolve, EXPRESS_CHECKOUT_NOTICE_DELAY )
);
};
4 changes: 1 addition & 3 deletions client/stripe-utils/cash-app-limit-notice-handler.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}

/**
Expand Down Expand Up @@ -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() ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
Expand All @@ -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',
];
}

Expand Down
2 changes: 2 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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).
Loading

0 comments on commit 86ab175

Please sign in to comment.