diff --git a/changelog.txt b/changelog.txt index c5c951ffbe..c6fabe6c62 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ * Dev - Update SCSS to replace @import with @use and @forward * Fix - Handle missing customer when calling payment_methods API * Dev - Fix some e2e issues: timing, optional flows, and WooCommerce RC support +* Fix - Reduce number of calls to Stripe payment_methods API = 9.7.1 - 2025-07-28 = * Add - Add state mapping for Lithuania in express checkout diff --git a/includes/class-wc-stripe-customer.php b/includes/class-wc-stripe-customer.php index 11c658c437..4b5c244c4b 100644 --- a/includes/class-wc-stripe-customer.php +++ b/includes/class-wc-stripe-customer.php @@ -30,6 +30,13 @@ class WC_Stripe_Customer { WC_Stripe_UPE_Payment_Method_Becs_Debit::STRIPE_ID, ]; + /** + * The maximum value for the `limit` argument in the Stripe payment_methods API. + * + * @see https://docs.stripe.com/api/payment_methods/customer_list#list_customer_payment_methods-limit + */ + protected const PAYMENT_METHODS_API_LIMIT = 100; + /** * Stripe customer ID * @@ -712,7 +719,7 @@ public function get_payment_methods( $payment_method_type ) { [ 'customer' => $this->get_id(), 'type' => $payment_method_type, - 'limit' => 100, // Maximum allowed value. + 'limit' => self::PAYMENT_METHODS_API_LIMIT, ], 'payment_methods' . $params, 'GET' @@ -741,6 +748,93 @@ public function get_payment_methods( $payment_method_type ) { return empty( $payment_methods ) ? [] : $payment_methods; } + /** + * Get all payment methods for a customer. + * + * @param string[] $payment_method_types The payment method types to look for using Stripe method IDs. If the array is empty, it implies all payment method types. + * @param int $limit The maximum number of payment methods to return. If the value is -1, no limit is applied. + * @return array + */ + public function get_all_payment_methods( array $payment_method_types = [], int $limit = -1 ) { + if ( ! $this->get_id() ) { + return []; + } + + $cache_key = self::PAYMENT_METHODS_TRANSIENT_KEY . '__all_' . $this->get_id(); + $all_payment_methods = get_transient( $cache_key ); + + if ( false === $all_payment_methods || ! is_array( $all_payment_methods ) ) { + $all_payment_methods = []; + $last_payment_method_id = null; + + do { + $request_params = [ + 'customer' => $this->get_id(), + 'limit' => self::PAYMENT_METHODS_API_LIMIT, + ]; + + if ( $last_payment_method_id ) { + $request_params['starting_after'] = $last_payment_method_id; + } + + $response = WC_Stripe_API::request( $request_params, 'payment_methods?expand[]=data.sepa_debit.generated_from.charge&expand[]=data.sepa_debit.generated_from.setup_attempt', 'GET' ); + + if ( ! empty( $response->error ) ) { + if ( + isset( $response->error->param, $response->error->code ) + && 'customer' === $response->error->param + && 'resource_missing' === $response->error->code + ) { + // If the customer doesn't exist, cache an empty array. + set_transient( $cache_key, [], DAY_IN_SECONDS ); + } + return []; + } + + if ( ! is_array( $response->data ) || [] === $response->data ) { + break; + } + + $all_payment_methods = array_merge( $all_payment_methods, $response->data ); + + // Reset the last payment method ID so we can paginate correctly. + $last_payment_method_id = null; + if ( isset( $response->has_more ) && $response->has_more ) { + $last_payment_method = end( $response->data ); + + if ( $last_payment_method && ! empty( $last_payment_method->id ) ) { + $last_payment_method_id = $last_payment_method->id; + } + } + } while ( null !== $last_payment_method_id ); + + // Always cache the result without any filters applied. + set_transient( $cache_key, $all_payment_methods, DAY_IN_SECONDS ); + } + + // If there are no payment methods, no need to apply any filters below. + if ( [] === $all_payment_methods ) { + return $all_payment_methods; + } + + // Only apply the limit and type filters after fetching and caching all payment methods. + $filtered_payment_methods = $all_payment_methods; + if ( [] !== $payment_method_types ) { + $filtered_payment_methods = array_filter( + $filtered_payment_methods, + function ( $payment_method ) use ( $payment_method_types ) { + return in_array( $payment_method->type, $payment_method_types, true ); + } + ); + } + + if ( $limit > 0 ) { + return array_slice( $filtered_payment_methods, 0, $limit ); + } + + return $filtered_payment_methods; + } + /** * Delete a source from stripe. * @@ -846,6 +940,7 @@ public function clear_cache( $payment_method_id = null ) { foreach ( self::STRIPE_PAYMENT_METHODS as $payment_method_type ) { delete_transient( self::PAYMENT_METHODS_TRANSIENT_KEY . $payment_method_type . $this->get_id() ); } + delete_transient( self::PAYMENT_METHODS_TRANSIENT_KEY . '__all_' . $this->get_id() ); // Clear cache for the specific payment method if provided. if ( $payment_method_id ) { WC_Stripe_Database_Cache::delete( 'payment_method_for_source_' . $payment_method_id ); diff --git a/includes/payment-tokens/class-wc-stripe-payment-tokens.php b/includes/payment-tokens/class-wc-stripe-payment-tokens.php index 839d9b9654..1aeed475dc 100644 --- a/includes/payment-tokens/class-wc-stripe-payment-tokens.php +++ b/includes/payment-tokens/class-wc-stripe-payment-tokens.php @@ -301,44 +301,24 @@ public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, // Retrieve the payment methods for the enabled reusable gateways. $payment_methods = []; - if ( $gateway->is_oc_enabled() ) { - // For OC, get all available payment method types - foreach ( self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD as $payment_method_type => $reausable_gateway_id ) { - $payment_method_instance = WC_Stripe_UPE_Payment_Gateway::get_payment_method_instance( $payment_method_type ); - if ( $payment_method_instance ) { - $retrieved_methods = $customer->get_payment_methods( $payment_method_type ); - if ( ! empty( $retrieved_methods ) ) { - $payment_methods[] = $retrieved_methods; - } - } - } - } else { - foreach ( self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD as $payment_method_type => $reausable_gateway_id ) { - // The payment method type doesn't match the ones we use. Nothing to do here. - if ( ! isset( $gateway->payment_methods[ $payment_method_type ] ) ) { - continue; - } - $payment_method_instance = $gateway->payment_methods[ $payment_method_type ]; - if ( $payment_method_instance->is_enabled() ) { - $payment_methods[] = $customer->get_payment_methods( $payment_method_type ); - } - } - } + $reusable_payment_method_types = array_keys( self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD ); + + $enabled_payment_methods = $gateway->get_upe_enabled_payment_method_ids(); + $active_reusable_payment_method_types = array_intersect( $enabled_payment_methods, $reusable_payment_method_types ); // Add SEPA if it is disabled and iDEAL or Bancontact are enabled. iDEAL and Bancontact tokens are saved as SEPA tokens. - if ( $gateway->is_sepa_tokens_for_other_methods_enabled() ) { - if ( $gateway->is_oc_enabled() ) { - $payment_methods[] = $customer->get_payment_methods( WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID ); - } elseif ( ! $gateway->payment_methods[ WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID ]->is_enabled() - && ( $gateway->payment_methods[ WC_Stripe_UPE_Payment_Method_Ideal::STRIPE_ID ]->is_enabled() - || $gateway->payment_methods[ WC_Stripe_UPE_Payment_Method_Bancontact::STRIPE_ID ]->is_enabled() ) ) { - - $payment_methods[] = $customer->get_payment_methods( WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID ); + if ( $gateway->is_sepa_tokens_for_other_methods_enabled() && ! in_array( WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID, $active_reusable_payment_method_types, true ) ) { + if ( + in_array( WC_Stripe_UPE_Payment_Method_Ideal::STRIPE_ID, $active_reusable_payment_method_types, true ) + || in_array( WC_Stripe_UPE_Payment_Method_Bancontact::STRIPE_ID, $active_reusable_payment_method_types, true ) + ) { + $active_reusable_payment_method_types[] = WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID; } } - $payment_methods = array_merge( ...$payment_methods ); + $payment_methods = $customer->get_all_payment_methods( $active_reusable_payment_method_types ); + $payment_method_ids = array_map( function ( $payment_method ) { return $payment_method->id; diff --git a/readme.txt b/readme.txt index aff87fe292..03c8be67ba 100644 --- a/readme.txt +++ b/readme.txt @@ -146,5 +146,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Dev - Update SCSS to replace @import with @use and @forward * Fix - Handle missing customer when calling payment_methods API * Dev - Fix some e2e issues: timing, optional flows, and WooCommerce RC support +* Fix - Reduce number of calls to Stripe payment_methods API [See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).