diff --git a/changelog.txt b/changelog.txt
index 61f5d8ea53..9ad43217af 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -7,6 +7,7 @@
* Update - Remove BACS from the unsupported 'change payment method for subscription' page.
* Fix - Fix payment method title display when new payment settings experience is enabled
* Fix - Prevent styles from non-checkout pages affecting the appearance of Stripe element.
+* Fix - Prevent further Stripe API calls if API keys are invalid (401 response).
= 9.5.0 - 2025-05-13 =
* Fix - Fixes the listing of payment methods on the classic checkout when the Optimized Checkout is enabled.
diff --git a/client/settings/account-details/index.js b/client/settings/account-details/index.js
index 510834e3d4..1be50467ad 100644
--- a/client/settings/account-details/index.js
+++ b/client/settings/account-details/index.js
@@ -133,11 +133,11 @@ const AccountDetails = () => {
{ createInterpolateElement(
isTestModeEnabled
? __(
- "Seems like the test API keys we've saved for you are no longer valid. If you recently updated them, use the Configure Connection button below to reconnect.",
+ "We couldn't connect to your account, it seems like the test API keys we've saved for you are no longer valid. Please use the Configure connection button below to reconnect.",
'woocommerce-gateway-stripe'
)
: __(
- "Seems like the live API keys we've saved for you are no longer valid. If you recently updated them, use the Configure Connection button below to reconnect.",
+ "We couldn't connect to your account, it seems like the live API keys we've saved for you are no longer valid. Please use the Configure connection button below to reconnect.",
'woocommerce-gateway-stripe'
),
{
diff --git a/client/settings/stripe-auth-account/account-status-panel.js b/client/settings/stripe-auth-account/account-status-panel.js
index d7f8702be8..1e424ed34a 100644
--- a/client/settings/stripe-auth-account/account-status-panel.js
+++ b/client/settings/stripe-auth-account/account-status-panel.js
@@ -150,7 +150,7 @@ const getAccountStatus = ( accountKeys, data, testMode ) => {
},
};
- if ( ! hasKeys ) {
+ if ( ! hasKeys || data?.account === null ) {
return accountStatusMap.disconnected;
}
diff --git a/includes/class-wc-stripe-api.php b/includes/class-wc-stripe-api.php
index 0ce455bb3d..c0ea516d1f 100644
--- a/includes/class-wc-stripe-api.php
+++ b/includes/class-wc-stripe-api.php
@@ -16,6 +16,20 @@ class WC_Stripe_API {
const ENDPOINT = 'https://api.stripe.com/v1/';
const STRIPE_API_VERSION = '2024-06-20';
+ /**
+ * The test mode invalid API keys option key.
+ *
+ * @var string
+ */
+ const TEST_MODE_INVALID_API_KEYS_OPTION_KEY = 'wc_stripe_test_invalid_api_keys_detected';
+
+ /**
+ * The live mode invalid API keys option key.
+ *
+ * @var string
+ */
+ const LIVE_MODE_INVALID_API_KEYS_OPTION_KEY = 'wc_stripe_live_invalid_api_keys_detected';
+
/**
* Secret API Key.
*
@@ -231,6 +245,13 @@ public static function request( $request, $api = 'charges', $method = 'POST', $w
* @param string $api
*/
public static function retrieve( $api ) {
+ // If we have an option flag indicating that the secret key is not valid, we don't attempt the API call and we return an error.
+ $invalid_api_keys_option_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_INVALID_API_KEYS_OPTION_KEY : self::LIVE_MODE_INVALID_API_KEYS_OPTION_KEY;
+ $invalid_api_keys_detected = get_option( $invalid_api_keys_option_key );
+ if ( $invalid_api_keys_detected ) {
+ return null; // The UI expects this empty response in case of invalid API keys.
+ }
+
WC_Stripe_Logger::log( "{$api}" );
$response = wp_safe_remote_get(
@@ -242,6 +263,18 @@ public static function retrieve( $api ) {
]
);
+ // If we get a 401 error, we know the secret key is not valid.
+ if ( is_array( $response ) && isset( $response['response'] ) && is_array( $response['response'] ) && isset( $response['response']['code'] ) && 401 === $response['response']['code'] ) {
+ // We save a flag in the options to avoid making calls until the secret key gets updated.
+ update_option( $invalid_api_keys_option_key, true );
+ update_option( $invalid_api_keys_option_key . '_at', time() );
+
+ // We delete the transient for the account data to trigger the not-connected UI in the admin dashboard.
+ delete_transient( WC_Stripe_Mode::is_test() ? WC_Stripe_Account::TEST_ACCOUNT_OPTION : WC_Stripe_Account::LIVE_ACCOUNT_OPTION );
+
+ return null; // The UI expects this empty response in case of invalid API keys.
+ }
+
if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
WC_Stripe_Logger::log( 'Error Response: ' . print_r( $response, true ) );
return new WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'woocommerce-gateway-stripe' ) );
diff --git a/includes/connect/class-wc-stripe-connect.php b/includes/connect/class-wc-stripe-connect.php
index bf82252826..58e662ac0e 100644
--- a/includes/connect/class-wc-stripe-connect.php
+++ b/includes/connect/class-wc-stripe-connect.php
@@ -183,6 +183,11 @@ private function save_stripe_keys( $result, $type = 'connect', $mode = 'live' )
update_option( 'wc_stripe_' . $prefix . 'oauth_failed_attempts', 0 );
update_option( 'wc_stripe_' . $prefix . 'oauth_last_failed_at', '' );
+ // Clear the invalid API keys transient.
+ $invalid_api_keys_option_key = $is_test ? WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY : WC_Stripe_API::LIVE_MODE_INVALID_API_KEYS_OPTION_KEY;
+ update_option( $invalid_api_keys_option_key, false );
+ update_option( $invalid_api_keys_option_key . '_at', time() );
+
if ( 'app' === $type ) {
// Stripe App OAuth access_tokens expire after 1 hour:
// https://docs.stripe.com/stripe-apps/api-authentication/oauth#refresh-access-token
diff --git a/readme.txt b/readme.txt
index 5e7858b583..09e9baca4e 100644
--- a/readme.txt
+++ b/readme.txt
@@ -118,6 +118,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
* Update - Remove BACS from the unsupported 'change payment method for subscription' page.
* Fix - Fix payment method title display when new payment settings experience is enabled
* Fix - Prevent styles from non-checkout pages affecting the appearance of Stripe element.
+* Fix - Prevent further Stripe API calls if API keys are invalid (401 response).
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).
diff --git a/tests/phpunit/test-class-wc-stripe-api.php b/tests/phpunit/test-class-wc-stripe-api.php
index 61a64aacf4..f860a63c57 100644
--- a/tests/phpunit/test-class-wc-stripe-api.php
+++ b/tests/phpunit/test-class-wc-stripe-api.php
@@ -94,4 +94,88 @@ public function test_set_secret_key_for_mode_with_parameter() {
WC_Stripe_API::set_secret_key_for_mode( 'invalid' );
$this->assertEquals( self::LIVE_SECRET_KEY, WC_Stripe_API::get_secret_key() );
}
+
+ /**
+ * Test WC_Stripe_API::retrieve() when API returns 401 error.
+ */
+ public function test_retrieve_handles_401_error() {
+ // Mock a 401 API response
+ add_filter( 'pre_http_request', [ $this, 'mock_401_response' ] );
+
+ // Call the retrieve method
+ $result = WC_Stripe_API::retrieve( 'test_endpoint' );
+
+ // Verify the result is null
+ $this->assertNull( $result );
+
+ // Verify the invalid API keys option was set
+ $this->assertTrue( get_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY ) );
+
+ // Clean up
+ remove_filter( 'pre_http_request', [ $this, 'mock_401_response' ] );
+ delete_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY );
+ }
+
+ /**
+ * Test WC_Stripe_API::retrieve() when API keys are invalid.
+ */
+ public function test_retrieve_returns_null_when_api_keys_are_invalid() {
+ // Set up the invalid API keys option
+ update_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY, true );
+
+ // Call the retrieve method
+ $result = WC_Stripe_API::retrieve( 'test_endpoint' );
+
+ // Verify the result is null
+ $this->assertNull( $result );
+
+ // Clean up
+ delete_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY );
+ }
+
+ /**
+ * Test WC_Stripe_API::retrieve() when API keys are valid.
+ */
+ public function test_retrieve_makes_api_call_when_api_keys_are_valid() {
+ // Ensure no invalid API keys option exists
+ delete_option( WC_Stripe_API::TEST_MODE_INVALID_API_KEYS_OPTION_KEY );
+
+ // Mock a successful API response
+ add_filter( 'pre_http_request', [ $this, 'mock_successful_response' ] );
+
+ // Call the retrieve method
+ $result = WC_Stripe_API::retrieve( 'test_endpoint' );
+
+ // Verify the result matches our mock response
+ $this->assertEquals( 'success', $result );
+
+ // Clean up
+ remove_filter( 'pre_http_request', [ $this, 'mock_successful_response' ] );
+ }
+
+ /**
+ * Helper method to mock a successful API response.
+ */
+ public function mock_successful_response() {
+ return [
+ 'response' => [
+ 'code' => 200,
+ 'message' => 'OK',
+ ],
+ 'body' => json_encode( 'success' ),
+ ];
+ }
+
+ /**
+ * Helper method to mock a 401 API response.
+ */
+ public function mock_401_response() {
+ return [
+ 'response' => [
+ 'code' => 401,
+ 'message' => 'Unauthorized',
+ ],
+ 'body' => '',
+ ];
+ }
}