diff --git a/includes/class-wc-stripe-customer.php b/includes/class-wc-stripe-customer.php index b6e5a036c3..20f524cb8c 100644 --- a/includes/class-wc-stripe-customer.php +++ b/includes/class-wc-stripe-customer.php @@ -704,32 +704,90 @@ public function get_payment_methods( $payment_method_type ) { return []; } - $payment_methods = get_transient( self::PAYMENT_METHODS_TRANSIENT_KEY . $payment_method_type . $this->get_id() ); + return $this->get_all_payment_methods( [ $payment_method_type ] ); + } - if ( false === $payment_methods ) { - $params = WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID === $payment_method_type ? '?expand[]=data.sepa_debit.generated_from.charge&expand[]=data.sepa_debit.generated_from.setup_attempt' : ''; - $response = WC_Stripe_API::request( - [ + /** + * 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(), - 'type' => $payment_method_type, - 'limit' => 100, // Maximum allowed value. - ], - 'payment_methods' . $params, - 'GET' - ); + 'limit' => 100, + ]; - if ( ! empty( $response->error ) ) { - return []; - } + if ( $last_payment_method_id ) { + $request_params['starting_after'] = $last_payment_method_id; + } - if ( is_array( $response->data ) ) { - $payment_methods = $response->data; - } + $response = WC_Stripe_API::request( $request_params, 'payment_methods', 'GET' ); + + if ( ! empty( $response->error ) ) { + if ( + isset( $response->error->code ) + && isset( $response->error->param ) + && '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 ); + } + + // Note that we 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 ); + } + ); + } - set_transient( self::PAYMENT_METHODS_TRANSIENT_KEY . $payment_method_type . $this->get_id(), $payment_methods, DAY_IN_SECONDS ); + if ( $limit > 0 ) { + return array_slice( $filtered_payment_methods, 0, $limit ); } - return empty( $payment_methods ) ? [] : $payment_methods; + return $filtered_payment_methods; } /** @@ -837,6 +895,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/compat/trait-wc-stripe-subscriptions.php b/includes/compat/trait-wc-stripe-subscriptions.php index 3078684c53..023de3df71 100644 --- a/includes/compat/trait-wc-stripe-subscriptions.php +++ b/includes/compat/trait-wc-stripe-subscriptions.php @@ -1016,69 +1016,69 @@ public function maybe_render_subscription_payment_method( $payment_method_to_dis $payment_method_to_display = __( 'N/A', 'woocommerce-gateway-stripe' ); try { - // Retrieve all possible payment methods for subscriptions. - foreach ( WC_Stripe_Customer::STRIPE_PAYMENT_METHODS as $payment_method_type ) { - foreach ( $stripe_customer->get_payment_methods( $payment_method_type ) as $source ) { - if ( $source->id !== $stripe_source_id ) { - continue; - } + // Retrieve all possible payment methods for the customer so we make minimal API calls to Stripe. + $customer_payment_methods = $stripe_customer->get_all_payment_methods(); + + foreach ( $customer_payment_methods as $source ) { + if ( $source->id !== $stripe_source_id ) { + continue; + } + + // Legacy handling for Stripe Card objects. ref: https://docs.stripe.com/api/cards/object + if ( isset( $source->object ) && WC_Stripe_Payment_Methods::CARD === $source->object ) { + /* translators: 1) card brand 2) last 4 digits */ + $payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $source->brand ) ? wc_get_credit_card_type_label( $source->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $source->last4 ); + break; + } - // Legacy handling for Stripe Card objects. ref: https://docs.stripe.com/api/cards/object - if ( isset( $source->object ) && WC_Stripe_Payment_Methods::CARD === $source->object ) { + switch ( $source->type ) { + case WC_Stripe_Payment_Methods::CARD: /* translators: 1) card brand 2) last 4 digits */ - $payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $source->brand ) ? wc_get_credit_card_type_label( $source->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $source->last4 ); + $payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $source->card->brand ) ? wc_get_credit_card_type_label( $source->card->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $source->card->last4 ); + break 2; + case WC_Stripe_Payment_Methods::SEPA_DEBIT: + /* translators: 1) last 4 digits of SEPA Direct Debit */ + $payment_method_to_display = sprintf( __( 'Via SEPA Direct Debit ending in %1$s', 'woocommerce-gateway-stripe' ), $source->sepa_debit->last4 ); + break 2; + case WC_Stripe_Payment_Methods::CASHAPP_PAY: + /* translators: 1) Cash App Cashtag */ + $payment_method_to_display = sprintf( __( 'Via Cash App Pay (%1$s)', 'woocommerce-gateway-stripe' ), $source->cashapp->cashtag ); + break 2; + case WC_Stripe_Payment_Methods::LINK: + /* translators: 1) email address associated with the Stripe Link payment method */ + $payment_method_to_display = sprintf( __( 'Via Stripe Link (%1$s)', 'woocommerce-gateway-stripe' ), $source->link->email ); + break 2; + case WC_Stripe_Payment_Methods::ACH: + $payment_method_to_display = sprintf( + /* translators: 1) account type (checking, savings), 2) last 4 digits of account. */ + __( 'Via %1$s Account ending in %2$s', 'woocommerce-gateway-stripe' ), + ucfirst( $source->us_bank_account->account_type ), + $source->us_bank_account->last4 + ); + break 2; + case WC_Stripe_Payment_Methods::BECS_DEBIT: + $payment_method_to_display = sprintf( + /* translators: last 4 digits of account. */ + __( 'BECS Direct Debit ending in %s', 'woocommerce-gateway-stripe' ), + $source->au_becs_debit->last4 + ); + break 2; + case WC_Stripe_Payment_Methods::ACSS_DEBIT: + $payment_method_to_display = sprintf( + /* translators: 1) bank name, 2) last 4 digits of account. */ + __( 'Via %1$s ending in %2$s', 'woocommerce-gateway-stripe' ), + $source->acss_debit->bank_name, + $source->acss_debit->last4 + ); + break 2; + case WC_Stripe_Payment_Methods::BACS_DEBIT: + /* translators: 1) the Bacs Direct Debit payment method's last 4 numbers */ + $payment_method_to_display = sprintf( __( 'Via Bacs Direct Debit ending in (%1$s)', 'woocommerce-gateway-stripe' ), $source->bacs_debit->last4 ); + break 2; + case WC_Stripe_Payment_Methods::AMAZON_PAY: + /* translators: 1) the Amazon Pay payment method's email */ + $payment_method_to_display = sprintf( __( 'Via Amazon Pay (%1$s)', 'woocommerce-gateway-stripe' ), $source->billing_details->email ?? '' ); break 2; - } - - switch ( $source->type ) { - case WC_Stripe_Payment_Methods::CARD: - /* translators: 1) card brand 2) last 4 digits */ - $payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $source->card->brand ) ? wc_get_credit_card_type_label( $source->card->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $source->card->last4 ); - break 3; - case WC_Stripe_Payment_Methods::SEPA_DEBIT: - /* translators: 1) last 4 digits of SEPA Direct Debit */ - $payment_method_to_display = sprintf( __( 'Via SEPA Direct Debit ending in %1$s', 'woocommerce-gateway-stripe' ), $source->sepa_debit->last4 ); - break 3; - case WC_Stripe_Payment_Methods::CASHAPP_PAY: - /* translators: 1) Cash App Cashtag */ - $payment_method_to_display = sprintf( __( 'Via Cash App Pay (%1$s)', 'woocommerce-gateway-stripe' ), $source->cashapp->cashtag ); - break 3; - case WC_Stripe_Payment_Methods::LINK: - /* translators: 1) email address associated with the Stripe Link payment method */ - $payment_method_to_display = sprintf( __( 'Via Stripe Link (%1$s)', 'woocommerce-gateway-stripe' ), $source->link->email ); - break 3; - case WC_Stripe_Payment_Methods::ACH: - $payment_method_to_display = sprintf( - /* translators: 1) account type (checking, savings), 2) last 4 digits of account. */ - __( 'Via %1$s Account ending in %2$s', 'woocommerce-gateway-stripe' ), - ucfirst( $source->us_bank_account->account_type ), - $source->us_bank_account->last4 - ); - break 3; - case WC_Stripe_Payment_Methods::BECS_DEBIT: - $payment_method_to_display = sprintf( - /* translators: last 4 digits of account. */ - __( 'BECS Direct Debit ending in %s', 'woocommerce-gateway-stripe' ), - $source->au_becs_debit->last4 - ); - break 3; - case WC_Stripe_Payment_Methods::ACSS_DEBIT: - $payment_method_to_display = sprintf( - /* translators: 1) bank name, 2) last 4 digits of account. */ - __( 'Via %1$s ending in %2$s', 'woocommerce-gateway-stripe' ), - $source->acss_debit->bank_name, - $source->acss_debit->last4 - ); - break 3; - case WC_Stripe_Payment_Methods::BACS_DEBIT: - /* translators: 1) the Bacs Direct Debit payment method's last 4 numbers */ - $payment_method_to_display = sprintf( __( 'Via Bacs Direct Debit ending in (%1$s)', 'woocommerce-gateway-stripe' ), $source->bacs_debit->last4 ); - break 3; - case WC_Stripe_Payment_Methods::AMAZON_PAY: - /* translators: 1) the Amazon Pay payment method's email */ - $payment_method_to_display = sprintf( __( 'Via Amazon Pay (%1$s)', 'woocommerce-gateway-stripe' ), $source->billing_details->email ?? '' ); - break 3; - } } } } catch ( WC_Stripe_Exception $e ) { diff --git a/includes/payment-tokens/class-wc-stripe-payment-tokens.php b/includes/payment-tokens/class-wc-stripe-payment-tokens.php index 839d9b9654..9ad2a7a4e6 100644 --- a/includes/payment-tokens/class-wc-stripe-payment-tokens.php +++ b/includes/payment-tokens/class-wc-stripe-payment-tokens.php @@ -301,19 +301,18 @@ public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, // Retrieve the payment methods for the enabled reusable gateways. $payment_methods = []; + $reusable_payment_method_types = array_keys( self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD ); + $active_payment_method_types = []; 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 ) { + // For Optimized Checkout, get all available payment method types. + foreach ( $reusable_payment_method_types as $payment_method_type ) { $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; - } + $active_payment_method_types[] = $payment_method_type; } } } else { - foreach ( self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD as $payment_method_type => $reausable_gateway_id ) { + foreach ( $reusable_payment_method_types as $payment_method_type ) { // The payment method type doesn't match the ones we use. Nothing to do here. if ( ! isset( $gateway->payment_methods[ $payment_method_type ] ) ) { continue; @@ -321,10 +320,11 @@ public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, $payment_method_instance = $gateway->payment_methods[ $payment_method_type ]; if ( $payment_method_instance->is_enabled() ) { - $payment_methods[] = $customer->get_payment_methods( $payment_method_type ); + $active_payment_method_types[] = $payment_method_type; } } } + $payment_methods = $customer->get_all_payment_methods( $active_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() ) { diff --git a/tests/phpunit/WC_Stripe_Payment_Gateway_Test.php b/tests/phpunit/WC_Stripe_Payment_Gateway_Test.php index 5ff56f4b6c..3c3e2cfcc7 100644 --- a/tests/phpunit/WC_Stripe_Payment_Gateway_Test.php +++ b/tests/phpunit/WC_Stripe_Payment_Gateway_Test.php @@ -595,7 +595,7 @@ public function test_render_subscription_payment_method() { $mock_subscription->save(); // This is the key the customer's payment methods are stored under in the transient. - $transient_key = WC_Stripe_Customer::PAYMENT_METHODS_TRANSIENT_KEY . 'cardcus_mock'; + $transient_key = WC_Stripe_Customer::PAYMENT_METHODS_TRANSIENT_KEY . '__all_cus_mock'; $mock_payment_method = new stdClass(); $mock_payment_method->id = 'src_mock';