Skip to content
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* Fix - Klarna not processing recurring payments
* Fix - Fix Express Checkout error with free trial subscription on blocks cart/checkout
* Fix - Prevent retrying requests that errored out due to declined payment methods
* Fix - Detect WooCommerce Subscriptions staging sites when checking if payments can be detached

= 10.0.1 - 2025-10-15 =
* Fix - Remove persistent reconnection notices
Expand Down
48 changes: 38 additions & 10 deletions includes/class-wc-stripe-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -576,21 +576,49 @@ public static function should_detach_payment_method_from_customer() {
return true;
}

// Return true for the delete user request from the admin dashboard or WP-CLI when the site is a production site
// and return false when the site is a staging/local/development site.
// This is to avoid detaching the payment method from the live production site.
// Requests coming from the customer account page i.e delete payment method, are not affected by this and returns true.
if ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
if ( 'production' === wp_get_environment_type() ) {
return true;
} else {
return false;
}
// Requests coming from the customer account page i.e delete payment method, should always be allowed, and should return true.
$is_admin_request = is_admin() || ( defined( 'WP_CLI' ) && WP_CLI );
if ( ! $is_admin_request ) {
return true;
}

// If we are not in a production site, we should not detach the payment method,
// as we don't want to detach the payment method from the live production site.
$is_staging_site = self::is_woocommerce_subscriptions_staging_mode() || 'production' !== wp_get_environment_type();
if ( $is_staging_site ) {
return false;
}

// Otherwise, we are in a production site, and we should detach the payment method.
return true;
}

/**
* Checks if the site has WooCommerce Subscriptions staging mode enabled.
*
* @return bool True if the site has WooCommerce Subscriptions active and staging mode enabled, false otherwise.
*/
private static function is_woocommerce_subscriptions_staging_mode() {
if ( ! class_exists( 'WC_Subscriptions' ) ) {
return false;
}

// Check if WooCommerce Subscriptions >= 4.0.0 is active (uses WCS_Staging class)
if ( class_exists( 'WCS_Staging' ) && method_exists( 'WCS_Staging', 'is_duplicate_site' ) ) {
return WCS_Staging::is_duplicate_site();
}

// Check if WooCommerce Subscriptions < 4.0.0 is active
// and if it is, check if the site is in staging mode via is_duplicate_site().
if ( version_compare( WC_Subscriptions::$version, '4.0.0', '<' )
&& method_exists( 'WC_Subscriptions', 'is_duplicate_site' )
) {
return WC_Subscriptions::is_duplicate_site();
}

return false;
}

/**
* Get the payment method configuration.
*
Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
* Fix - Klarna not processing recurring payments
* Fix - Fix Express Checkout error with free trial subscription on blocks cart/checkout
* Fix - Prevent retrying requests that errored out due to declined payment methods
* Fix - Detect WooCommerce Subscriptions staging sites when checking if payments can be detached

[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).
34 changes: 34 additions & 0 deletions tests/phpunit/Helpers/WCS_Staging.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* Helper class to mimic the WCS_Staging class from WooCommerce Subscriptions.
* ONLY for use in unit tests!
*/
class WCS_Staging {

/**
* Local flag to indicate whether the site is a duplicate site.
*
* @var bool
*/
private static bool $is_duplicate_site = false;

/**
* Helper function to set the value of $is_duplicate_site for tests.
*
* @param bool $is_duplicate_site Whether the site is a duplicate site.
* @return void
*/
public static function set_is_duplicate_site( bool $is_duplicate_site ): void {
self::$is_duplicate_site = $is_duplicate_site;
}

/**
* Mimic WCS_Staging::is_duplicate_site().
*
* @return bool
*/
public static function is_duplicate_site(): bool {
return self::$is_duplicate_site;
}
}
80 changes: 80 additions & 0 deletions tests/phpunit/WC_Stripe_API_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,84 @@ public function mock_unauthorized_response() {
'body' => json_encode( [ 'error' => 'invalid_api_key' ] ),
];
}

public function provide_test_should_detach_payment_method_from_customer(): array {
return [
'test mode from non-admin context should detach' => [
'expected_return' => true,
'is_test_mode' => true,
'is_admin_request' => false,
'is_wc_sub_staging_site' => false,
],
'live mode from non-admin context should detach' => [
'expected_return' => true,
'is_test_mode' => false,
'is_admin_request' => false,
'is_wc_sub_staging_site' => false,
],
'test mode from admin context should detach' => [
'expected_return' => true,
'is_test_mode' => true,
'is_admin_request' => true,
'is_wc_sub_staging_site' => false,
],
'live mode from admin context with no subscription staging site should detach' => [
'expected_return' => true,
'is_test_mode' => false,
'is_admin_request' => true,
'is_wc_sub_staging_site' => false,
],
'live mode from admin context with subscription staging site should not detach' => [
'expected_return' => false,
'is_test_mode' => false,
'is_admin_request' => true,
'is_wc_sub_staging_site' => true,
],
// Ideally, we would test multiple environment types, but wp_get_environment_type() uses a
// static variable that can't be modified between tests.
];
}

/**
* @dataProvider provide_test_should_detach_payment_method_from_customer
*/
public function test_should_detach_payment_method_from_customer( bool $expected_return, bool $is_test_mode, bool $is_admin_request, bool $is_wc_sub_staging_site = false ) {
$initial_test_mode = \WC_Stripe_Mode::is_test();

$stripe_settings = \WC_Stripe_Helper::get_stripe_settings();
$stripe_settings['testmode'] = $is_test_mode ? 'yes' : 'no';
\WC_Stripe_Helper::update_main_stripe_settings( $stripe_settings );

$initial_current_screen = null;
$reset_current_screen = false;

if ( $is_admin_request ) {
$initial_current_screen = $GLOBALS['current_screen'] ?? null;
$reset_current_screen = true;

// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$GLOBALS['current_screen'] = \WP_Screen::get( 'post.php' );
}

require_once __DIR__ . '/Helpers/WCS_Staging.php';
\WCS_Staging::set_is_duplicate_site( $is_wc_sub_staging_site );

$result = \WC_Stripe_API::should_detach_payment_method_from_customer();

// Reset the environment before running any assertions.
if ( $reset_current_screen ) {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$GLOBALS['current_screen'] = $initial_current_screen;
}

if ( $initial_test_mode !== $is_test_mode ) {
$stripe_settings = \WC_Stripe_Helper::get_stripe_settings();
$stripe_settings['testmode'] = $initial_test_mode ? 'yes' : 'no';
\WC_Stripe_Helper::update_main_stripe_settings( $stripe_settings );
}

\WCS_Staging::set_is_duplicate_site( false );

$this->assertEquals( $expected_return, $result );
}
}
Loading