Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion includes/class-wc-stripe-customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ 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.
*/
protected const PAYMENT_METHODS_API_LIMIT = 100;

/**
* Stripe customer ID
*
Expand Down Expand Up @@ -712,7 +717,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'
Expand Down Expand Up @@ -741,6 +746,94 @@ 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();
Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The cache key uses a hardcoded '_all' separator which could conflict with existing keys or be unclear. Consider using a more descriptive separator or adding it as a class constant for consistency.

Suggested change
$cache_key = self::PAYMENT_METHODS_TRANSIENT_KEY . '__all_' . $this->get_id();
$cache_key = self::PAYMENT_METHODS_TRANSIENT_KEY . self::PAYMENT_METHODS_ALL_SEPARATOR . $this->get_id();

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our long-term plan here should just be to use one cache, as most users don't have a large number of saved payment methods, and I think most of our use cases revolve around all saved payment methods rather than saved payment methods for only one particular type of payment.

$all_payment_methods = get_transient( $cache_key );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use the WC_Stripe_Database_Cache here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but I was wary of adding new code and adding new data to the database cache before we have a cleanup mechanism (which I created an issue for earlier today in #4569).

For large stores with regular customers, using the database cache would result in a big jump in the options size, which could easily drive other performance issues on sites.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also want to pull @diegocurbelo into this thread as he flagged the persistent cache usage as well in his review.

I've implemented calling WC_Stripe_Database_Cache in #4570, mostly so we can merge that change into this branch if we decide to go in that direction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For large stores with regular customers, using the database cache would result in a big jump in the options size, which could easily drive other performance issues on sites.

Expired transients are not automatically removed from the DB; that requires custom code (calling delete_expired_transients ()) or using a plugin... and each transient adds two items to the options table (one with the data, and one with the expiration), the DB cache uses one so it would be half the amount of entries than using transients.

I can create a quick PR that adds an additional options entry for the expiration timestamp in the private write_to_cache() method, and then add a new public method delete_expired() to WC_Stripe_Database_Cache similar to this one. And schedule it to run every 24 hours (filterable).

This would generate the same number of options entries as using transients, and provide a way for custom code to remove expired cache items, what do you think @daledupreez?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I am concerned about timelines and stability, and what changes should (and should not) be included in 9.8.0. Maybe it makes sense to have a quick sync with the team to work things out?


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' );
Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded expand parameters in the API request URL make the code inflexible. Consider extracting these parameters into a constant or making them configurable to improve maintainability.

Suggested change
$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' );
$response = WC_Stripe_API::request( $request_params, 'payment_methods?' . self::PAYMENT_METHODS_EXPAND_PARAMS, 'GET' );

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to, but that should be tackled in a follow-up PR.


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 ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is getting lengthy, I would consider creating new private methods to handle different parts of the logic.

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.
*
Expand Down Expand Up @@ -846,6 +939,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 );
Expand Down
26 changes: 15 additions & 11 deletions includes/payment-tokens/class-wc-stripe-payment-tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,44 +301,48 @@ 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;
}

$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;
}
}
}

// 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 );
if ( ! in_array( WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID, $active_payment_method_types, true ) ) {
$active_payment_method_types[] = 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 );
$active_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_payment_method_types );

$payment_method_ids = array_map(
function ( $payment_method ) {
return $payment_method->id;
Expand Down
Loading